版本历史与代码示例之JMX

开发 后端
对于一个正在运行的Java程序,我们希望管理和监控它的状态,如:内存、CPU使用率、线程数、垃圾回收情况等等,这时使用JMX便是一种非常优雅的解决方案。

[[429706]]

前言

对于一个正在运行的Java程序,我们希望管理和监控它的状态,如:内存、CPU使用率、线程数、垃圾回收情况等等,这时使用JMX便是一种非常优雅的解决方案。你可能听过JConsole、VisualVM等性能调优工具,殊不知哥俩底层都依赖于它,本文就带你走进Java的管理扩展:JMX。

[[429707]]

JMX既是Java管理系统的一个标准,一个规范;也是一个接口,一个“框架”。有标准、有规范是为了让开发者可以定制开发自己的扩展功能,而且作为一个“框架”来讲,JDK 已经帮我们实现了常用的功能,尤其是对JVM本身的监控和管理。

所属专栏【方向盘】

-Java EE

相关下载

  • 【本专栏源代码】:https://github.com/yourbatman/FXP-java-ee
  • 【技术专栏源代码大本营】:https://github.com/yourbatman/tech-column-learning
  • 【女娲Knife-Initializr工程】访问地址:http://152.136.106.14:8761
  • 【程序员专用网盘】公益上线啦,注册送1G超小容量,帮你实践做减法:https://wangpan.yourbatman.cn
  • 【Java开发软件包(Mac)】:https://wangpan.yourbatman.cn/s/rEH0 提取码:javakit

版本约定

  • Java EE:6、7、8
  • Jakarta EE:8、9、9.1

正文

JMX

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。我们可以使用jmx对程序的运行状态进行监控和管理。

JMX是Java EE内嵌(被内嵌进JRE里面了)的一套标准的代理和服务,也就是说只要遵循这个接口标准,那么就可以管理和监控我们的应用程序。为了标准化管理和监控,Java平台使用JMX作为管理和监控的标准接口,任何程序,只要按JMX规范访问这个接口,就可以获取所有管理与监控信息。常用的运维监控如Zabbix、Nagios等工具对JVM本身的监控都是通过JMX获取的信息。

JMX是一个标准接口,不但可以用于管理JVM,还可以管理应用程序自身。

这是官方给出的JMX架构图:

由图可知,JMX技术分为三层:

设备/资源层:这些被管理的资源就是MBean/MXBean们

代理层:MBeanServer就是代理层的最核心组件,MBean们均注册到此处,让它代理统一对外提供功能服务

  • 代理层其实就是一个独立的Java线程

远程管理层:JMX技术可以通过多种不同的方式去访问,每个适配器通过一个给定的协议来访问MBeanServer中注册的所有MBean们,比如Html协议、Http协议、JDK自己实现的RMI协议等

什么是MBean

MBean = Managed Bean。其的本质就是我们经常说的Java Bean,遵循Java Bean规范,只是它专门用于JMX所以称为MBean。JMX把所有被管理的资源都称为MBean,全部交由MBeanServer管理,JVM会将自身各种资源(CPU、内存等)注册到JMX中,自己也可自定义MBean然后放进去,从而达到自定义监控的能力。最后对外通过暴露RMI/HTTP协议提供访问。

  • 说明:JMX不需要安装任何额外组件,也不需要第三方库,因为MBeanServer已经内置在JavaSE标准库中了。

JDK提供的MBean主要都在java.lang.management 和 javax.management这两个包里面,MBean一共分为四种类型:

1.Standard MBean:最常用、最简单的一种,结构和普通Java Bean没有区别,管理接口通过方法名来描述。它只要遵循一定的命名规则即可注册进MBeanServer

  • 定义一个接口,该接口名称必须为xxxMBean(必须以MBean为后缀结尾)
  • 写该接口的实现类,然后将此实现类注册进MBeanServer即可

2.Dynamic MBean:在运行期才定义它的属性和方法,也就是说它有什么属性和方法是可以动态改变的。所有的动态MBean必须实现DynamicMBean接口,然后注册上去即可

  • 动态Bean的辅助类主要有MBeanConstructorInfo、MBeanAttributeInfo、MBeanOperationInfo等等
  • 动态Bean是一种妥协的产物,因为已经存在一些MBean,而将其改造成标准MBean比较费力而且不切实际,所以就用动态Bean妥协一下。自定义的时候几乎不会使用

3.Open MBean:Open MBeans需实现DynamicMBean接口,与动态Bean不同的是提供了更复杂的metadata数据,和在接口中,只使用了几种预定义的通用数据类型:OpenMBeanInfo、OpenMBeanOperationInfo、OpenMBeanConstructorInfo、OpenMBeanParameterInfo、OpenMBeanAttributeInfo

4.Model MBean:如果不能修改已有的Java类,使用它是个不错的选择。通过实现接口javax.management.modelmbean.RequiredModelMBean,我们要做的就是实例化该类然后注册即可实现对资源的管理

  • 编写Model MBean的最大挑战是告诉Model MBean对象托管资源的那些熟悉和方法可以暴露给代理层,ModelMBeanInfo对象描述了将会暴露给代理的构造函数、属性、操作甚至是监听器。

话外音:一般情况下,我们只需要了解Standard MBean即可。

MBean和MXBean区别

MBean与MXBean的区别主要是在于在接口中会引用到一些其他类型的类(复合类型)时,其表现方式的不一样。

  • MBean:属性不能是复合类型/自定义类型,否则不能被识别
  • MXBean:属性可以是自定义类型。如JDK自带的MemoryMXBean中定义了heapMemoryUsage属性,它就是复合类型

什么是MBeanServer

顾名思义:用于管理MBean的“服务器”。一般来讲一个JVM只有一个MBeanServer(通过ManagementFactory.getPlatformMBeanServer()这个API来获得),用于管理该JVM内所有的MBean,并且对外提供服务。

倘若需要多个MBeanServer(比如不同的domain),你可通过MBeanServerFactory.newMBeanServer(String domain)这个API来创建。

什么是Connector和Adaptor

当MBean都注册到MBeanServer上面后,功能已经具备,就可以通过协议把这些功能暴露出去啦。针对不同的协议就有其对应的Connector或者Adaptor(这里可把Connector和Adaptor认为是相同的角色)。

所以,只要有连接器/适配器,可以通过多种协议将功能暴露出去,如Http协议、Saop协议、RMI等。JDK默认实现的只有基于RMI的javax.management.remote.rmi.RMIConnector,像JConsole、VisualVM这类工具默认是可直接连接访问的。

注意:Spring Boot Actuator对其管理、监控等端点提供Http和RMI(JMX)两种访问方式,但是其Http方式并非实现了Connector/Adaptor哦,甚至来讲基于Http的操作方式都并非JMX方式(实为Endpoint方式),不要让某些文章给误导了哈。

既然有Http,JMX意义何在?

这个问题一度困扰过我,没太想明白JMX存在的意义。诚然,JMX能完成的任务通过Http都能完成,只不过某些情况下用JMX来做会更加方便。简单来讲,Http更重,JMX更轻。

  • Http是一个更加抽象、应用面更广泛、功能更强大的协议/服务,因此做的工作也会多一些。比如光方法它就有Get、Post、Put、Delete等等
  • JMX是一个更加具体、应用面不那么广、功能也没有Http强大的协议/服务。所以它的优点是轻便、好用

JMX的特点决定了它非常非常适合做资源监控,因此各大监控组件、框架为了监控JVM的运行情况,都会把JMX当做首选,而Http协议只是为了产品化的备选。

  1. jmx被内嵌入jdk/jre自带,无需额外导包 

版本历程

JMX伴随着JDK 5的发布而出现,之后其实也几乎没有变化,如下所示。

Java EE 5:

Java EE 8:

JSR 3的内容基本和JSR 255没变,可认为一样。

 生存现状

高阶必备。比如做监控、JVM性能分析、调优、问题定位等。

实现(框架)

代码示例

虽说Demo示例才是重头戏,但由于本文并非JMX专题,所以只会示例原生方式使用JMX,至于在Spring、Spring Boot、借助commons-modeler等使用,点到即止。

直接使用JDK内置的MBean/MXBean

JDK内置了“大量”的MBean,供你直接使用:

  • ClassLoadingMXBean:Java虚拟机的类加载系统。
  • CompilationMXBean:Java虚拟机的编译系统。
  • MemoryMXBean:Java虚拟机的内存系统。
  • ThreadMXBean:Java虚拟机的线程系统。
  • RuntimeMXBean:Java虚拟机的运行时系统。
  • OperatingSystemMXBean:Java虚拟机在其上运行的操作系统。
  • GarbageCollectorMXBean:Java虚拟机中的垃圾回收器。
  • MemoryManagerMXBean:Java虚拟机中的内存管理器。
  • MemoryPoolMXBean:Java虚拟机中的内存池。

这些实例通过ManagementFactory都可拿到。

  1. @Test 
  2. public void test1() { 
  3.     ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); 
  4.     ObjectName objectName = classLoadingMXBean.getObjectName(); 
  5.     long totalLoadedClassCount = classLoadingMXBean.getTotalLoadedClassCount(); 
  6.     int loadedClassCount = classLoadingMXBean.getLoadedClassCount(); 
  7.     long unloadedClassCount = classLoadingMXBean.getUnloadedClassCount(); 
  8.  
  9.     System.out.println("objectName:" + objectName); 
  10.     System.out.println("JVM启动共加载的Class类总数(一个类被加载多次):" + totalLoadedClassCount); 
  11.     System.out.println("JVM当前状态加载Class类总数:" + loadedClassCount); 
  12.     System.out.println("JVM还未加载的Class类总数:" + unloadedClassCount); 
  13.  
  14. objectName:java.lang:type=ClassLoading 
  15. JVM启动共加载的Class类总数(一个类被加载多次):1743 
  16. JVM当前状态加载Class类总数:1743 
  17. JVM还未加载的Class类总数:0 
  1. @Test 
  2. public void test2() { 
  3.     RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 
  4.     ObjectName objectName = runtimeMXBean.getObjectName(); 
  5.     String name = runtimeMXBean.getName(); 
  6.     // JVM信息 
  7.     String specVendor = runtimeMXBean.getSpecVendor(); 
  8.     String specName = runtimeMXBean.getSpecName(); 
  9.     String specVersion = runtimeMXBean.getSpecVersion(); 
  10.  
  11.     String bootClassPath = runtimeMXBean.getBootClassPath(); 
  12.     String classPath = runtimeMXBean.getClassPath(); 
  13.     String libraryPath = runtimeMXBean.getLibraryPath(); 
  14.  
  15.     System.out.println("objectName:" + objectName); 
  16.     System.out.println("运行期名称name:" + name); 
  17.     System.out.println("当前JVM进程ID:" + name.split("@")[0]); 
  18.     System.out.println("虚拟机信息:" + specVendor + ":" + specName + ":" + specVersion); 
  19.     // System.out.println("bootClassPath:" + bootClassPath); 
  20.     // System.out.println("classPath:" + classPath); 
  21.     // System.out.println("libraryPath:" + libraryPath); 
  22.  
  23. objectName:java.lang:type=Runtime 
  24. 运行期名称name:9966@YourBatman-MBA.local 
  25. 当前JVM进程ID:9966 
  26. 虚拟机信息:Oracle Corporation:Java Virtual Machine Specification:1.8 

RuntimeMXBean它常被用来获取JVM进程ID。

  1. @Test 
  2. public void test3() { 
  3.     // JVM内存情况 
  4.     MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); 
  5.     ObjectName objectName = memoryMXBean.getObjectName(); 
  6.     MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); 
  7.     MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage(); 
  8.  
  9.     System.out.println("objectName:" + objectName); 
  10.     System.out.println("已使用堆内存:" + heapMemoryUsage); 
  11.     System.out.println("已使用非堆内存:" + nonHeapMemoryUsage); 
  12.  
  13.     // 操作系统的内存情况? 
  14.     long l = Runtime.getRuntime().totalMemory(); 
  15.     long l1 = Runtime.getRuntime().freeMemory(); 
  16.  
  17. objectName:java.lang:type=Memory 
  18. 已使用堆内存:init = 268435456(262144K) used = 24183016(23616K) committed = 257425408(251392K) max = 3817865216(3728384K) 
  19. 已使用非堆内存:init = 2555904(2496K) used = 12547040(12252K) committed = 13959168(13632K) max = -1(-1K) 

下面OperatingSystemMXBean是操作系统层面的信息:

  1. @Test 
  2. public void test4() { 
  3.     OperatingSystemMXBean osbean = ManagementFactory.getOperatingSystemMXBean(); 
  4.     System.out.println("操作系统体系结构:" + osbean.getArch()); 
  5.     System.out.println("操作系统名字:" + osbean.getName()); 
  6.     System.out.println("处理器数目:" + osbean.getAvailableProcessors()); 
  7.     System.out.println("操作系统版本:" + osbean.getVersion()); 
  8.  
  9.     ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); 
  10.     System.out.println("总线程数:" + threadBean.getThreadCount());// 
  11.  
  12. 操作系统体系结构:aarch64 
  13. 操作系统名字:Mac OS X 
  14. 处理器数目:8 
  15. 操作系统版本:11.6 
  16. 总线程数:4 

自定义MBean - 本地线程连接

除了以上系统自带的MBean/MXBean,更重要的是自定义MBean:将普通User实体类暴露成为一个MBean。

  1. /** 
  2.  * MBean资源通过接口暴露,【一定必须】以MBean结尾才算一个MBean 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/10/18 21:14 
  7.  * @since 0.0.1 
  8.  */ 
  9. public interface UserMBean { 
  10.  
  11.     String getName(); 
  12.  
  13.     void setName(String name); 
  14.  
  15.     void setAge(int age); 

User实体类必须实现此接口:

  1. @Getter 
  2. @Setter 
  3. public class User implements UserMBean { 
  4.  
  5.     private String name
  6.     private int age; 
  7.  

将此MBean注册到MBeanServer:

  1. @Test 
  2. public void test1() throws Exception { 
  3.     MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); 
  4.     ObjectName objectName = new ObjectName("com.yourbatman:type=UserXXX"); // 名字可任意取,但最好见名知意 
  5.     mBeanServer.registerMBean(new User(), objectName); 
  6.  
  7.     // 线程保活,方便获取MBean 
  8.     Thread.sleep(Long.MAX_VALUE); 

使用JConsole即可连接到此线程:

链接上后即可以进行“操作”啦:

自定义MBean - 远程连接

除了通过本地进程连接外,JDK原生还支持通过RMI协议暴露,供以连接。我们只需要将其通过RMI协议暴露出去即可:

JMX并不限制通过上面协议暴露出去,只是JDK默认只实现了RMI协议,够用就好!

  1. @Test 
  2. public void test2() throws Exception { 
  3.     MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); 
  4.  
  5.     LocateRegistry.createRegistry(9090); // 这一步不能少,不需要返回值 
  6.     JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9090/userXXX"); 
  7.     JMXConnectorServer cntorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer); 
  8.     cntorServer.start(); 
  9.  
  10.     ObjectName objectName = new ObjectName("com.yourbatman:type=UserXXX"); 
  11.     mBeanServer.registerMBean(new User(), objectName); 
  12.  
  13.     // 线程保活,方便获取MBean 
  14.     Thread.sleep(Long.MAX_VALUE); 

使用JConsole通过RMI协议远程连接:

自定义MBean - 编程方式连接

除了通过JConsole这类工具连接外,通过编程方式也是能够通过JMX搞的。毕竟RMI协议用Java可以直接操作嘛:

  1. @Test 
  2. public void test1() throws Exception { 
  3.     JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9090/userXXX"); 
  4.     JMXConnector conn = JMXConnectorFactory.connect(url, null); 
  5.  
  6.     UserMBean userMBean = JMX.newMBeanProxy(conn.getMBeanServerConnection(), new ObjectName("com.yourbatman:type=UserXXX"), UserMBean.class); 
  7.     System.out.println("通过RMI协议拿到:" + userMBean); 
  8.     System.out.println("user的名字:" + userMBean.getName()); 
  9.  
  10.     conn.close(); 
  11.  
  12. 通过RMI协议拿到:MBeanProxy(javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@706a04ae[com.yourbatman:type=UserXXX]) 
  13. user的名字:null 

注意:执行client前请确保Server端已启动,否则会连接失败!

自定义MBean - 远程连接(启动参数方式)

对于已打好的Jar包/war包,不可能改其代码再让其支持JMX远程连接。这时,我们可以通过启动参数方式来开启远程连接。这些启动参数一般放在命令行、环境变量里。

  1. java  
  2.  
  3. -Djava.rmi.server.hostname=你的主机 
  4. -Dcom.sun.management.jmxremote.port=端口号 
  5. -Dcom.sun.management.jmxremote.ssl=false 
  6. -Dcom.sun.management.jmxremote.authenticate=false 
  7.  
  8. -jar xxx.jar 

总结

JMX是Java EE规范、JDK提供的一个小工具,使用起来不难但能量不小,推荐你可花点时间学习学习、写一写、用一用以发挥效用,向高级进阶。

其实JMX并不“稀有”,它存在于很多流行软件/中间件里:Kafka、Spring Boot、RocketMQ,以及Logback都可看到JMX的影子,实现了很好的功能。如:使用JMX(无需重启)动态更改Logback的日志级别。

关于JMX的内容,本文点到即止。若你在Spring/Spring Boot场景下开发,依托于Spring的抽象能力,“集成/使用”JMX将变得更加容易,期待你的探索,以后有机会我们再聊此专题。

本文转载自微信公众号「Java方向盘」

 

责任编辑:姜华 来源: Java方向盘
相关推荐

2021-09-15 18:54:22

BATutopia-JWebSocket

2021-10-11 08:51:50

JavaMailJDBCJava

2021-10-25 08:16:20

Java JAX-RS Java 基础

2021-09-13 18:39:50

ServeltELJSP

2021-10-08 06:50:32

版本历史代码

2024-09-03 10:35:31

JMXJava框架

2017-10-31 12:56:52

Androidios谷歌

2023-09-05 07:02:25

开源工具应用程序

2023-09-04 00:05:27

JMX管理组件

2010-04-09 18:02:31

Oracle创建

2024-10-09 17:12:34

2010-03-23 14:12:43

Python开发Win

2011-08-16 16:37:40

Oracle数据库树形查询根节点

2011-08-25 14:38:14

SQL Server修改表结构字段类型

2010-04-16 09:27:18

Ocacle执行计划

2010-03-23 13:30:36

Python VIM

2018-01-02 13:30:04

代码质量代码预言

2023-11-09 16:13:00

TypeScript前端

2024-06-03 00:00:20

.NET定时器

2010-01-28 16:01:18

Android Jni
点赞
收藏

51CTO技术栈公众号