前言
对于一个正在运行的Java程序,我们希望管理和监控它的状态,如:内存、CPU使用率、线程数、垃圾回收情况等等,这时使用JMX便是一种非常优雅的解决方案。你可能听过JConsole、VisualVM等性能调优工具,殊不知哥俩底层都依赖于它,本文就带你走进Java的管理扩展:JMX。
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协议只是为了产品化的备选。
- 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都可拿到。
- @Test
- public void test1() {
- ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
- ObjectName objectName = classLoadingMXBean.getObjectName();
- long totalLoadedClassCount = classLoadingMXBean.getTotalLoadedClassCount();
- int loadedClassCount = classLoadingMXBean.getLoadedClassCount();
- long unloadedClassCount = classLoadingMXBean.getUnloadedClassCount();
- System.out.println("objectName:" + objectName);
- System.out.println("JVM启动共加载的Class类总数(一个类被加载多次):" + totalLoadedClassCount);
- System.out.println("JVM当前状态加载Class类总数:" + loadedClassCount);
- System.out.println("JVM还未加载的Class类总数:" + unloadedClassCount);
- }
- objectName:java.lang:type=ClassLoading
- JVM启动共加载的Class类总数(一个类被加载多次):1743
- JVM当前状态加载Class类总数:1743
- JVM还未加载的Class类总数:0
- @Test
- public void test2() {
- RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
- ObjectName objectName = runtimeMXBean.getObjectName();
- String name = runtimeMXBean.getName();
- // JVM信息
- String specVendor = runtimeMXBean.getSpecVendor();
- String specName = runtimeMXBean.getSpecName();
- String specVersion = runtimeMXBean.getSpecVersion();
- String bootClassPath = runtimeMXBean.getBootClassPath();
- String classPath = runtimeMXBean.getClassPath();
- String libraryPath = runtimeMXBean.getLibraryPath();
- System.out.println("objectName:" + objectName);
- System.out.println("运行期名称name:" + name);
- System.out.println("当前JVM进程ID:" + name.split("@")[0]);
- System.out.println("虚拟机信息:" + specVendor + ":" + specName + ":" + specVersion);
- // System.out.println("bootClassPath:" + bootClassPath);
- // System.out.println("classPath:" + classPath);
- // System.out.println("libraryPath:" + libraryPath);
- }
- objectName:java.lang:type=Runtime
- 运行期名称name:9966@YourBatman-MBA.local
- 当前JVM进程ID:9966
- 虚拟机信息:Oracle Corporation:Java Virtual Machine Specification:1.8
RuntimeMXBean它常被用来获取JVM进程ID。
- @Test
- public void test3() {
- // JVM内存情况
- MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
- ObjectName objectName = memoryMXBean.getObjectName();
- MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
- MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
- System.out.println("objectName:" + objectName);
- System.out.println("已使用堆内存:" + heapMemoryUsage);
- System.out.println("已使用非堆内存:" + nonHeapMemoryUsage);
- // 操作系统的内存情况?
- long l = Runtime.getRuntime().totalMemory();
- long l1 = Runtime.getRuntime().freeMemory();
- }
- objectName:java.lang:type=Memory
- 已使用堆内存:init = 268435456(262144K) used = 24183016(23616K) committed = 257425408(251392K) max = 3817865216(3728384K)
- 已使用非堆内存:init = 2555904(2496K) used = 12547040(12252K) committed = 13959168(13632K) max = -1(-1K)
下面OperatingSystemMXBean是操作系统层面的信息:
- @Test
- public void test4() {
- OperatingSystemMXBean osbean = ManagementFactory.getOperatingSystemMXBean();
- System.out.println("操作系统体系结构:" + osbean.getArch());
- System.out.println("操作系统名字:" + osbean.getName());
- System.out.println("处理器数目:" + osbean.getAvailableProcessors());
- System.out.println("操作系统版本:" + osbean.getVersion());
- ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
- System.out.println("总线程数:" + threadBean.getThreadCount());//
- }
- 操作系统体系结构:aarch64
- 操作系统名字:Mac OS X
- 处理器数目:8
- 操作系统版本:11.6
- 总线程数:4
自定义MBean - 本地线程连接
除了以上系统自带的MBean/MXBean,更重要的是自定义MBean:将普通User实体类暴露成为一个MBean。
- /**
- * MBean资源通过接口暴露,【一定必须】以MBean结尾才算一个MBean
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
- * @site https://yourbatman.cn
- * @date 2021/10/18 21:14
- * @since 0.0.1
- */
- public interface UserMBean {
- String getName();
- void setName(String name);
- void setAge(int age);
- }
User实体类必须实现此接口:
- @Getter
- @Setter
- public class User implements UserMBean {
- private String name;
- private int age;
- }
将此MBean注册到MBeanServer:
- @Test
- public void test1() throws Exception {
- MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
- ObjectName objectName = new ObjectName("com.yourbatman:type=UserXXX"); // 名字可任意取,但最好见名知意
- mBeanServer.registerMBean(new User(), objectName);
- // 线程保活,方便获取MBean
- Thread.sleep(Long.MAX_VALUE);
- }
使用JConsole即可连接到此线程:
链接上后即可以进行“操作”啦:
自定义MBean - 远程连接
除了通过本地进程连接外,JDK原生还支持通过RMI协议暴露,供以连接。我们只需要将其通过RMI协议暴露出去即可:
JMX并不限制通过上面协议暴露出去,只是JDK默认只实现了RMI协议,够用就好!
- @Test
- public void test2() throws Exception {
- MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
- LocateRegistry.createRegistry(9090); // 这一步不能少,不需要返回值
- JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9090/userXXX");
- JMXConnectorServer cntorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer);
- cntorServer.start();
- ObjectName objectName = new ObjectName("com.yourbatman:type=UserXXX");
- mBeanServer.registerMBean(new User(), objectName);
- // 线程保活,方便获取MBean
- Thread.sleep(Long.MAX_VALUE);
- }
使用JConsole通过RMI协议远程连接:
自定义MBean - 编程方式连接
除了通过JConsole这类工具连接外,通过编程方式也是能够通过JMX搞的。毕竟RMI协议用Java可以直接操作嘛:
- @Test
- public void test1() throws Exception {
- JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:9090/userXXX");
- JMXConnector conn = JMXConnectorFactory.connect(url, null);
- UserMBean userMBean = JMX.newMBeanProxy(conn.getMBeanServerConnection(), new ObjectName("com.yourbatman:type=UserXXX"), UserMBean.class);
- System.out.println("通过RMI协议拿到:" + userMBean);
- System.out.println("user的名字:" + userMBean.getName());
- conn.close();
- }
- 通过RMI协议拿到:MBeanProxy(javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@706a04ae[com.yourbatman:type=UserXXX])
- user的名字:null
注意:执行client前请确保Server端已启动,否则会连接失败!
自定义MBean - 远程连接(启动参数方式)
对于已打好的Jar包/war包,不可能改其代码再让其支持JMX远程连接。这时,我们可以通过启动参数方式来开启远程连接。这些启动参数一般放在命令行、环境变量里。
- java
- -Djava.rmi.server.hostname=你的主机
- -Dcom.sun.management.jmxremote.port=端口号
- -Dcom.sun.management.jmxremote.ssl=false
- -Dcom.sun.management.jmxremote.authenticate=false
- -jar xxx.jar
总结
JMX是Java EE规范、JDK提供的一个小工具,使用起来不难但能量不小,推荐你可花点时间学习学习、写一写、用一用以发挥效用,向高级进阶。
其实JMX并不“稀有”,它存在于很多流行软件/中间件里:Kafka、Spring Boot、RocketMQ,以及Logback都可看到JMX的影子,实现了很好的功能。如:使用JMX(无需重启)动态更改Logback的日志级别。
关于JMX的内容,本文点到即止。若你在Spring/Spring Boot场景下开发,依托于Spring的抽象能力,“集成/使用”JMX将变得更加容易,期待你的探索,以后有机会我们再聊此专题。
本文转载自微信公众号「Java方向盘」