前言

对于一个正在运行的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将变得更加容易,期待你的探索,以后有机会我们再聊此专题。

dawei

【声明】:石嘴山站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。