版本歷史與代碼示例之JMX
前言
對(duì)于一個(gè)正在運(yùn)行的Java程序,我們希望管理和監(jiān)控它的狀態(tài),如:內(nèi)存、CPU使用率、線程數(shù)、垃圾回收情況等等,這時(shí)使用JMX便是一種非常優(yōu)雅的解決方案。你可能聽(tīng)過(guò)JConsole、VisualVM等性能調(diào)優(yōu)工具,殊不知哥倆底層都依賴于它,本文就帶你走進(jìn)Java的管理擴(kuò)展:JMX。
JMX既是Java管理系統(tǒng)的一個(gè)標(biāo)準(zhǔn),一個(gè)規(guī)范;也是一個(gè)接口,一個(gè)“框架”。有標(biāo)準(zhǔn)、有規(guī)范是為了讓開發(fā)者可以定制開發(fā)自己的擴(kuò)展功能,而且作為一個(gè)“框架”來(lái)講,JDK 已經(jīng)幫我們實(shí)現(xiàn)了常用的功能,尤其是對(duì)JVM本身的監(jiān)控和管理。
所屬專欄【方向盤】
-Java EE
相關(guān)下載
- 【本專欄源代碼】:https://github.com/yourbatman/FXP-java-ee
- 【技術(shù)專欄源代碼大本營(yíng)】:https://github.com/yourbatman/tech-column-learning
- 【女媧Knife-Initializr工程】訪問(wèn)地址:http://152.136.106.14:8761
- 【程序員專用網(wǎng)盤】公益上線啦,注冊(cè)送1G超小容量,幫你實(shí)踐做減法:https://wangpan.yourbatman.cn
- 【Java開發(fā)軟件包(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管理擴(kuò)展)是一個(gè)為應(yīng)用程序、設(shè)備、系統(tǒng)等植入管理功能的框架。我們可以使用jmx對(duì)程序的運(yùn)行狀態(tài)進(jìn)行監(jiān)控和管理。
JMX是Java EE內(nèi)嵌(被內(nèi)嵌進(jìn)JRE里面了)的一套標(biāo)準(zhǔn)的代理和服務(wù),也就是說(shuō)只要遵循這個(gè)接口標(biāo)準(zhǔn),那么就可以管理和監(jiān)控我們的應(yīng)用程序。為了標(biāo)準(zhǔn)化管理和監(jiān)控,Java平臺(tái)使用JMX作為管理和監(jiān)控的標(biāo)準(zhǔn)接口,任何程序,只要按JMX規(guī)范訪問(wèn)這個(gè)接口,就可以獲取所有管理與監(jiān)控信息。常用的運(yùn)維監(jiān)控如Zabbix、Nagios等工具對(duì)JVM本身的監(jiān)控都是通過(guò)JMX獲取的信息。
JMX是一個(gè)標(biāo)準(zhǔn)接口,不但可以用于管理JVM,還可以管理應(yīng)用程序自身。
這是官方給出的JMX架構(gòu)圖:
由圖可知,JMX技術(shù)分為三層:
設(shè)備/資源層:這些被管理的資源就是MBean/MXBean們
代理層:MBeanServer就是代理層的最核心組件,MBean們均注冊(cè)到此處,讓它代理統(tǒng)一對(duì)外提供功能服務(wù)
- 代理層其實(shí)就是一個(gè)獨(dú)立的Java線程
遠(yuǎn)程管理層:JMX技術(shù)可以通過(guò)多種不同的方式去訪問(wèn),每個(gè)適配器通過(guò)一個(gè)給定的協(xié)議來(lái)訪問(wèn)MBeanServer中注冊(cè)的所有MBean們,比如Html協(xié)議、Http協(xié)議、JDK自己實(shí)現(xiàn)的RMI協(xié)議等
什么是MBean
MBean = Managed Bean。其的本質(zhì)就是我們經(jīng)常說(shuō)的Java Bean,遵循Java Bean規(guī)范,只是它專門用于JMX所以稱為MBean。JMX把所有被管理的資源都稱為MBean,全部交由MBeanServer管理,JVM會(huì)將自身各種資源(CPU、內(nèi)存等)注冊(cè)到JMX中,自己也可自定義MBean然后放進(jìn)去,從而達(dá)到自定義監(jiān)控的能力。最后對(duì)外通過(guò)暴露RMI/HTTP協(xié)議提供訪問(wèn)。
- 說(shuō)明:JMX不需要安裝任何額外組件,也不需要第三方庫(kù),因?yàn)镸BeanServer已經(jīng)內(nèi)置在JavaSE標(biāo)準(zhǔn)庫(kù)中了。
JDK提供的MBean主要都在java.lang.management 和 javax.management這兩個(gè)包里面,MBean一共分為四種類型:
1.Standard MBean:最常用、最簡(jiǎn)單的一種,結(jié)構(gòu)和普通Java Bean沒(méi)有區(qū)別,管理接口通過(guò)方法名來(lái)描述。它只要遵循一定的命名規(guī)則即可注冊(cè)進(jìn)MBeanServer
- 定義一個(gè)接口,該接口名稱必須為xxxMBean(必須以MBean為后綴結(jié)尾)
- 寫該接口的實(shí)現(xiàn)類,然后將此實(shí)現(xiàn)類注冊(cè)進(jìn)MBeanServer即可
2.Dynamic MBean:在運(yùn)行期才定義它的屬性和方法,也就是說(shuō)它有什么屬性和方法是可以動(dòng)態(tài)改變的。所有的動(dòng)態(tài)MBean必須實(shí)現(xiàn)DynamicMBean接口,然后注冊(cè)上去即可
- 動(dòng)態(tài)Bean的輔助類主要有MBeanConstructorInfo、MBeanAttributeInfo、MBeanOperationInfo等等
- 動(dòng)態(tài)Bean是一種妥協(xié)的產(chǎn)物,因?yàn)橐呀?jīng)存在一些MBean,而將其改造成標(biāo)準(zhǔn)MBean比較費(fèi)力而且不切實(shí)際,所以就用動(dòng)態(tài)Bean妥協(xié)一下。自定義的時(shí)候幾乎不會(huì)使用
3.Open MBean:Open MBeans需實(shí)現(xiàn)DynamicMBean接口,與動(dòng)態(tài)Bean不同的是提供了更復(fù)雜的metadata數(shù)據(jù),和在接口中,只使用了幾種預(yù)定義的通用數(shù)據(jù)類型:OpenMBeanInfo、OpenMBeanOperationInfo、OpenMBeanConstructorInfo、OpenMBeanParameterInfo、OpenMBeanAttributeInfo
4.Model MBean:如果不能修改已有的Java類,使用它是個(gè)不錯(cuò)的選擇。通過(guò)實(shí)現(xiàn)接口javax.management.modelmbean.RequiredModelMBean,我們要做的就是實(shí)例化該類然后注冊(cè)即可實(shí)現(xiàn)對(duì)資源的管理
- 編寫Model MBean的最大挑戰(zhàn)是告訴Model MBean對(duì)象托管資源的那些熟悉和方法可以暴露給代理層,ModelMBeanInfo對(duì)象描述了將會(huì)暴露給代理的構(gòu)造函數(shù)、屬性、操作甚至是監(jiān)聽(tīng)器。
話外音:一般情況下,我們只需要了解Standard MBean即可。
MBean和MXBean區(qū)別
MBean與MXBean的區(qū)別主要是在于在接口中會(huì)引用到一些其他類型的類(復(fù)合類型)時(shí),其表現(xiàn)方式的不一樣。
- MBean:屬性不能是復(fù)合類型/自定義類型,否則不能被識(shí)別
- MXBean:屬性可以是自定義類型。如JDK自帶的MemoryMXBean中定義了heapMemoryUsage屬性,它就是復(fù)合類型
什么是MBeanServer
顧名思義:用于管理MBean的“服務(wù)器”。一般來(lái)講一個(gè)JVM只有一個(gè)MBeanServer(通過(guò)ManagementFactory.getPlatformMBeanServer()這個(gè)API來(lái)獲得),用于管理該JVM內(nèi)所有的MBean,并且對(duì)外提供服務(wù)。
倘若需要多個(gè)MBeanServer(比如不同的domain),你可通過(guò)MBeanServerFactory.newMBeanServer(String domain)這個(gè)API來(lái)創(chuàng)建。
什么是Connector和Adaptor
當(dāng)MBean都注冊(cè)到MBeanServer上面后,功能已經(jīng)具備,就可以通過(guò)協(xié)議把這些功能暴露出去啦。針對(duì)不同的協(xié)議就有其對(duì)應(yīng)的Connector或者Adaptor(這里可把Connector和Adaptor認(rèn)為是相同的角色)。
所以,只要有連接器/適配器,可以通過(guò)多種協(xié)議將功能暴露出去,如Http協(xié)議、Saop協(xié)議、RMI等。JDK默認(rèn)實(shí)現(xiàn)的只有基于RMI的javax.management.remote.rmi.RMIConnector,像JConsole、VisualVM這類工具默認(rèn)是可直接連接訪問(wèn)的。
注意:Spring Boot Actuator對(duì)其管理、監(jiān)控等端點(diǎn)提供Http和RMI(JMX)兩種訪問(wèn)方式,但是其Http方式并非實(shí)現(xiàn)了Connector/Adaptor哦,甚至來(lái)講基于Http的操作方式都并非JMX方式(實(shí)為Endpoint方式),不要讓某些文章給誤導(dǎo)了哈。
既然有Http,JMX意義何在?
這個(gè)問(wèn)題一度困擾過(guò)我,沒(méi)太想明白JMX存在的意義。誠(chéng)然,JMX能完成的任務(wù)通過(guò)Http都能完成,只不過(guò)某些情況下用JMX來(lái)做會(huì)更加方便。簡(jiǎn)單來(lái)講,Http更重,JMX更輕。
- Http是一個(gè)更加抽象、應(yīng)用面更廣泛、功能更強(qiáng)大的協(xié)議/服務(wù),因此做的工作也會(huì)多一些。比如光方法它就有Get、Post、Put、Delete等等
- JMX是一個(gè)更加具體、應(yīng)用面不那么廣、功能也沒(méi)有Http強(qiáng)大的協(xié)議/服務(wù)。所以它的優(yōu)點(diǎn)是輕便、好用
JMX的特點(diǎn)決定了它非常非常適合做資源監(jiān)控,因此各大監(jiān)控組件、框架為了監(jiān)控JVM的運(yùn)行情況,都會(huì)把JMX當(dāng)做首選,而Http協(xié)議只是為了產(chǎn)品化的備選。
- jmx被內(nèi)嵌入jdk/jre自帶,無(wú)需額外導(dǎo)包
版本歷程
JMX伴隨著JDK 5的發(fā)布而出現(xiàn),之后其實(shí)也幾乎沒(méi)有變化,如下所示。
Java EE 5:
Java EE 8:
JSR 3的內(nèi)容基本和JSR 255沒(méi)變,可認(rèn)為一樣。
生存現(xiàn)狀
高階必備。比如做監(jiān)控、JVM性能分析、調(diào)優(yōu)、問(wèn)題定位等。
實(shí)現(xiàn)(框架)
無(wú)
代碼示例
雖說(shuō)Demo示例才是重頭戲,但由于本文并非JMX專題,所以只會(huì)示例原生方式使用JMX,至于在Spring、Spring Boot、借助commons-modeler等使用,點(diǎn)到即止。
直接使用JDK內(nèi)置的MBean/MXBean
JDK內(nèi)置了“大量”的MBean,供你直接使用:
- ClassLoadingMXBean:Java虛擬機(jī)的類加載系統(tǒng)。
- CompilationMXBean:Java虛擬機(jī)的編譯系統(tǒng)。
- MemoryMXBean:Java虛擬機(jī)的內(nèi)存系統(tǒng)。
- ThreadMXBean:Java虛擬機(jī)的線程系統(tǒng)。
- RuntimeMXBean:Java虛擬機(jī)的運(yùn)行時(shí)系統(tǒng)。
- OperatingSystemMXBean:Java虛擬機(jī)在其上運(yùn)行的操作系統(tǒng)。
- GarbageCollectorMXBean:Java虛擬機(jī)中的垃圾回收器。
- MemoryManagerMXBean:Java虛擬機(jī)中的內(nèi)存管理器。
- MemoryPoolMXBean:Java虛擬機(jī)中的內(nèi)存池。
這些實(shí)例通過(guò)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啟動(dòng)共加載的Class類總數(shù)(一個(gè)類被加載多次):" + totalLoadedClassCount);
- System.out.println("JVM當(dāng)前狀態(tài)加載Class類總數(shù):" + loadedClassCount);
- System.out.println("JVM還未加載的Class類總數(shù):" + unloadedClassCount);
- }
- objectName:java.lang:type=ClassLoading
- JVM啟動(dòng)共加載的Class類總數(shù)(一個(gè)類被加載多次):1743
- JVM當(dāng)前狀態(tài)加載Class類總數(shù):1743
- JVM還未加載的Class類總數(shù):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("運(yùn)行期名稱name:" + name);
- System.out.println("當(dāng)前JVM進(jìn)程ID:" + name.split("@")[0]);
- System.out.println("虛擬機(jī)信息:" + specVendor + ":" + specName + ":" + specVersion);
- // System.out.println("bootClassPath:" + bootClassPath);
- // System.out.println("classPath:" + classPath);
- // System.out.println("libraryPath:" + libraryPath);
- }
- objectName:java.lang:type=Runtime
- 運(yùn)行期名稱name:9966@YourBatman-MBA.local
- 當(dāng)前JVM進(jìn)程ID:9966
- 虛擬機(jī)信息:Oracle Corporation:Java Virtual Machine Specification:1.8
RuntimeMXBean它常被用來(lái)獲取JVM進(jìn)程ID。
- @Test
- public void test3() {
- // JVM內(nèi)存情況
- MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
- ObjectName objectName = memoryMXBean.getObjectName();
- MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
- MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
- System.out.println("objectName:" + objectName);
- System.out.println("已使用堆內(nèi)存:" + heapMemoryUsage);
- System.out.println("已使用非堆內(nèi)存:" + nonHeapMemoryUsage);
- // 操作系統(tǒng)的內(nèi)存情況?
- long l = Runtime.getRuntime().totalMemory();
- long l1 = Runtime.getRuntime().freeMemory();
- }
- objectName:java.lang:type=Memory
- 已使用堆內(nèi)存:init = 268435456(262144K) used = 24183016(23616K) committed = 257425408(251392K) max = 3817865216(3728384K)
- 已使用非堆內(nèi)存:init = 2555904(2496K) used = 12547040(12252K) committed = 13959168(13632K) max = -1(-1K)
下面OperatingSystemMXBean是操作系統(tǒng)層面的信息:
- @Test
- public void test4() {
- OperatingSystemMXBean osbean = ManagementFactory.getOperatingSystemMXBean();
- System.out.println("操作系統(tǒng)體系結(jié)構(gòu):" + osbean.getArch());
- System.out.println("操作系統(tǒng)名字:" + osbean.getName());
- System.out.println("處理器數(shù)目:" + osbean.getAvailableProcessors());
- System.out.println("操作系統(tǒng)版本:" + osbean.getVersion());
- ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
- System.out.println("總線程數(shù):" + threadBean.getThreadCount());//
- }
- 操作系統(tǒng)體系結(jié)構(gòu):aarch64
- 操作系統(tǒng)名字:Mac OS X
- 處理器數(shù)目:8
- 操作系統(tǒng)版本:11.6
- 總線程數(shù):4
自定義MBean - 本地線程連接
除了以上系統(tǒng)自帶的MBean/MXBean,更重要的是自定義MBean:將普通User實(shí)體類暴露成為一個(gè)MBean。
- /**
- * MBean資源通過(guò)接口暴露,【一定必須】以MBean結(jié)尾才算一個(gè)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實(shí)體類必須實(shí)現(xiàn)此接口:
- @Getter
- @Setter
- public class User implements UserMBean {
- private String name;
- private int age;
- }
將此MBean注冊(cè)到MBeanServer:
- @Test
- public void test1() throws Exception {
- MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
- ObjectName objectName = new ObjectName("com.yourbatman:type=UserXXX"); // 名字可任意取,但最好見(jiàn)名知意
- mBeanServer.registerMBean(new User(), objectName);
- // 線程?;睿奖惬@取MBean
- Thread.sleep(Long.MAX_VALUE);
- }
使用JConsole即可連接到此線程:
鏈接上后即可以進(jìn)行“操作”啦:
自定義MBean - 遠(yuǎn)程連接
除了通過(guò)本地進(jìn)程連接外,JDK原生還支持通過(guò)RMI協(xié)議暴露,供以連接。我們只需要將其通過(guò)RMI協(xié)議暴露出去即可:
JMX并不限制通過(guò)上面協(xié)議暴露出去,只是JDK默認(rèn)只實(shí)現(xiàn)了RMI協(xié)議,夠用就好!
- @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通過(guò)RMI協(xié)議遠(yuǎn)程連接:
自定義MBean - 編程方式連接
除了通過(guò)JConsole這類工具連接外,通過(guò)編程方式也是能夠通過(guò)JMX搞的。畢竟RMI協(xié)議用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("通過(guò)RMI協(xié)議拿到:" + userMBean);
- System.out.println("user的名字:" + userMBean.getName());
- conn.close();
- }
- 通過(guò)RMI協(xié)議拿到:MBeanProxy(javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@706a04ae[com.yourbatman:type=UserXXX])
- user的名字:null
注意:執(zhí)行client前請(qǐng)確保Server端已啟動(dòng),否則會(huì)連接失敗!
自定義MBean - 遠(yuǎn)程連接(啟動(dòng)參數(shù)方式)
對(duì)于已打好的Jar包/war包,不可能改其代碼再讓其支持JMX遠(yuǎn)程連接。這時(shí),我們可以通過(guò)啟動(dòng)參數(shù)方式來(lái)開啟遠(yuǎn)程連接。這些啟動(dòng)參數(shù)一般放在命令行、環(huán)境變量里。
- java
- -Djava.rmi.server.hostname=你的主機(jī)
- -Dcom.sun.management.jmxremote.port=端口號(hào)
- -Dcom.sun.management.jmxremote.ssl=false
- -Dcom.sun.management.jmxremote.authenticate=false
- -jar xxx.jar
總結(jié)
JMX是Java EE規(guī)范、JDK提供的一個(gè)小工具,使用起來(lái)不難但能量不小,推薦你可花點(diǎn)時(shí)間學(xué)習(xí)學(xué)習(xí)、寫一寫、用一用以發(fā)揮效用,向高級(jí)進(jìn)階。
其實(shí)JMX并不“稀有”,它存在于很多流行軟件/中間件里:Kafka、Spring Boot、RocketMQ,以及Logback都可看到JMX的影子,實(shí)現(xiàn)了很好的功能。如:使用JMX(無(wú)需重啟)動(dòng)態(tài)更改Logback的日志級(jí)別。
關(guān)于JMX的內(nèi)容,本文點(diǎn)到即止。若你在Spring/Spring Boot場(chǎng)景下開發(fā),依托于Spring的抽象能力,“集成/使用”JMX將變得更加容易,期待你的探索,以后有機(jī)會(huì)我們?cè)倭拇藢n}。
本文轉(zhuǎn)載自微信公眾號(hào)「Java方向盤」