Java SE基础巩固(十三):JMX

1 概述

很多Java相关的基础书籍(包括Java核心技术I、II)都没有涉及到JMX的内容,以至于大多数Java学习者对JMX都不甚了解,但这个东西确实是Java SE的一部分,很多性能监控软件都是基于JMX规范开发的,例如jconsole。

JMX即Java Management Extensions(Java管理扩展),是Java SE的一部分,在Java2的时候加入到Java SE平台中,但Java5才正式发布。JMX提供了一个非常简单的途径去管理应用程序的资源,这里的所说的资源包括内存资源,磁盘资源等,而且因为JMX相关技术是动态的,所以可以在应用程序运行时监控和管理资源。

JMX主要由三大部分构成:

  • Managed Beans (MBean)代表了某种资源,MBean可以暴露一些接口,用于对MBean所代表的资源进行查看、修改等操作。

  • JMX agents (JMX代理)管理着一个或多个MBean,并将MBean暴露给客户端。JMX代理的核心组件是MBean Server,用于接受客户端的连接,并对客户端的请求做出响应。

  • Remoete Connectors 可以简单理解成客户端,最贴近用户,用于接受用户的请求,并将请求发送给JMX代理,然后将JMX代理的响应返回给用户。

举个例子,假设公司现在有一大堆粮食(有很多种类)需要管理,并设计了一套方案用来管理粮食。他们是这样设计的:用一类特殊的对象来这些粮食并且搭建了一个服务器,该服务器管理着这些粮食,而且还开发了一个对用户友好的程序供用户查看,购买各种各样的粮食。在这个例子中,用来表示粮食的对象就是MBean,而服务器就是JMX Aagent,对用户友好的程序其实就是客户端。

例子可能不太恰当,希望各位能理解其中的意思。

下面是JMX的架构图:

接下来我将就围绕这三大部分介绍JMX技术。

2 Managed Beans

Managed Beans简称MBean(后面就统一使用MBean了),一个MBean是一个被管理的Java对象,它和Java Bean有些相似,但不是完全等同,一个标准的MBean包含了以下内容:

  • 一系列可读的或者可写或者既可读又可写的属性。
  • 一系列可被调用的操作(方法)。
  • 描述自己的信息。

JMX规范定义了五种类型的MBean,分别是:

  • Standard MBeans
  • Dynamic MBeans
  • Open MBeans
  • Model MBeans
  • MXBeans

本文主要介绍的是Standard MBeans,至于其他类型的MBean,读者如果感兴趣,可以自行查找资料学习。

2.1 标准的MBean

下面是一个“标准”的MBean:

public interface HelloMBean {

    void sayHello();

    int add(int x, int y);

    String getName();

    void setCacheSize(int cacheSize);

    int getCacheSize();
}
public class Hello
        implements HelloMBean {

    private final String name = "Yeonon";

    private static final int DEFAULT_CACHE_SIZE = 200;
    private int cacheSize = DEFAULT_CACHE_SIZE;

    @Override
    public void sayHello() {
        System.out.println("hello, world!");
    }

    @Override
    public int add(int x, int y) {
        return x + y;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setCacheSize(int cacheSize) {
        int oldValue = this.cacheSize;
        this.cacheSize = cacheSize;
        System.out.println("Cache size now is : " + this.cacheSize);
    }

    @Override
    public int getCacheSize() {
        return this.cacheSize;
    }
}

标准的MBean通常都包含了一个以MBean为后缀的接口,以及其实现类,例如这里的HelloMBean以及其实现类Hello,在接口里定义了一系列的抽象方法,这些方法都是后面被暴露的,getter和setter方法的最主要作用是来表示某个字段的可读性和可写性,如果某个字段只包含了getter方法而没有包含setter方法,那么该字段就只是可读的,即无法通过JMX技术来修改该属性。在示例代码中,只定义了个操作,即sayHello和add(getter和setter虽然也是方法,但不把他们认为是操作),客户端可以在远程执行这些操作(调用这些方法),后面会看到一个具体的表现,在这里先不说。

2.2 通知

JMX还支持事件通知。当MBean的属性被修改、删除或者出现问题的时候,可以生成一个通知,并将其发送出去给,如果客户端包含了通知监听器,客户端就可以接受该通知并将其呈现给用户。MBean要支持通知机制,其实现类必须实现NotificationEmitter接口或者继承NotificationBroadcasterSupport类(推荐继承NotificationBroadcasterSupport类,因为该类已经实现了很多有用的方法),对上面的Hello类进行修改,修改之后的代码如下所示:

public class Hello
        extends NotificationBroadcasterSupport
        implements HelloMBean {

    private final String name = "Yeonon";

    private final AtomicLong sequenceNumber = new AtomicLong(0);

    private static final int DEFAULT_CACHE_SIZE = 200;
    private int cacheSize = DEFAULT_CACHE_SIZE;

    @Override
    public void sayHello() {
        System.out.println("hello, world!");
    }

    @Override
    public int add(int x, int y) {
        return x + y;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setCacheSize(int cacheSize) {
        int oldValue = this.cacheSize;
        this.cacheSize = cacheSize;
        System.out.println("Cache size now is : " + this.cacheSize);

        Notification notification = new AttributeChangeNotification(
                this, sequenceNumber.getAndIncrement(), System.currentTimeMillis(),
                "Cache Size changed", "CacheSize", "int",
                oldValue, this.cacheSize);
        sendNotification(notification);
    }

    @Override
    public int getCacheSize() {
        return this.cacheSize;
    }

    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        String[] types = new String[] {
                AttributeChangeNotification.ATTRIBUTE_CHANGE
        };
        String name = AttributeChangeNotification.class.getName();
        String description = "An attribute of this MBean has changed";
        MBeanNotificationInfo info = new MBeanNotificationInfo(types, name, description);
        return new MBeanNotificationInfo[]{info};
    }
}

最主要的修改是在setCacheSize方法里生成了一个Notification对象,该Notification对象就代表一个通知,Notification有很多已经编写好的实现类,例如这里的AttributeChangeNotification,因为这里要发送通知的原因就是属性被修改,所以就使用了这个实现类,该实现类的构造器签名如下所示:

    public AttributeChangeNotification(Object source, long sequenceNumber, long timeStamp, String msg,
                                       String attributeName, String attributeType, Object oldValue, Object newValue) {

        super(AttributeChangeNotification.ATTRIBUTE_CHANGE, source, sequenceNumber, timeStamp, msg);
        this.attributeName = attributeName;
        this.attributeType = attributeType;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

一共7个参数,这里我就说一个参数(其他参数根据参数名就可以知道是什么东西了),即sequenceNumber,从参数名上可以知道这是一个序列号,每一个通知都有一个序列化,主要是为了防止通知的顺序错误,和网络协议中的那个序列化有异曲同工之妙。例如,如果通知在JMX Agent和远程客户端之间的链路中传输时,发生了顺序错乱,有了序列号,客户端就可以知道应该先处理哪个通知,后处理哪个通知,避免乱序处理。

通知生成完毕之后,就调用sendNotification()方法将通知发送出去。你可能会有疑问?这个通知发送到哪去呢?答案是发送到客户端那里(默认是通过RMI机制发送),如果客户端有实现通知监听器,客户端就可以接受并处理通知了。

下面来看看JMX代理。

3 JMX代理

JMX代理的核心组件就是一个MBean Server,可以将MBean注册到MBean Server中,这就表示MBean由该MBean Server管理了。下面的代码演示了获取MBean Server并且将MBean注册到MBean Server的过程:

public class Main {

    public static void main(String[] args) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException {
        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("top.yeonon.jmx:type=Hello");
        Hello mbean = new Hello();
        mBeanServer.registerMBean(mbean, name);

        System.out.println("waiting forever....");
        while (true) {

        }
    }
}

可以通过ManagementFactory.getPlatformMBeanServer()来获取一个MBean Server实例对象,然后调用mBeanServer.registerMBean方法来把一个MBean注册到MBean Server中,该方法有两个参数,一个参数是MBean实例对象,第二个参数是对应MBean的名字,是ObjectName类型的对象,这里的名字其实并不一定就是top.yeonon.jmx:type=Hello,其格式是XXX:type=YYY,XXX和YYY都可以是任意的字符串,并不一定就是包名和类名,不过最好还是建议使用包名和类名,这样可以避免命名冲突。

启动程序,发现输出waiting forever....,程序没有关闭,其实就是进入无限循环了。这里先不着急,只要程序不包错就行了,下面我会带你来点刺激的!

4 JMX客户端

JMX客户端是最接近用户的,用户也许不知道后面的JMX Agent,MBean,但一定会知道客户端,JMX客户端不会直接和MBean打交道,而是通过JMX Agent来操作其管理的MBean。我们熟知的Jconsole就是一个JMX客户端,下面先介绍一下如何使用Jconsole来操作MBean,随后再介绍如何自定义一个JMX客户端。

4.1 Jconsole

保持JMX Agent处于运行状态,然后打开jconsole软件,如下所示:

选择对应的本地进程(后面会介绍如何使用远程进程来访问),点击之后看到如下界面:

选择MBean选项卡,然后找到top.yeonon.jmx选项,这里会看到有一个名字叫做Hello的MBean(注意到了吗,其实就是我们在JMX Agent里定义的那个ObjectName),现在选择cacheSize那一栏,在值那里修改可以看到当前cacheSize的值是200,修改成150,并点击刷新试试,如下图所示:

这时候回到控制台,发现多了一行输出:

Cache size now is : 150

说明现在cacheSize属性已经被修改成150了!我们成功的在程序运行时修改了属性。其他的操作,例如sayHello(),add等,各位可以自行尝试,在此就不多说了。

除了属性和操作,还可以看到有一个通知,选择通知那一栏,然后在右下边可以看到有一个“订阅”按钮,点击一下,表示想要接受通知,然后再次尝试修改CacheSize,此时会发现多了一行记录,如下所示:

消息的内容就是我们之前定义的,Jconsole只是将其展示出来了,并有做过多的处理。

4.2 自定义客户端

光是用别人开发的客户端没啥意思是不是,没事!JMX也提供了很方便的方式让我们编写自定义的客户端,下面是一个自定义客户端的代码:

package top.yeonon.jmx;

import javax.management.*;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Set;

import static java.lang.Thread.sleep;

/**
 * @Author yeonon
 * @date 2018/10/16 0016 20:47
 **/
public class Client {

    //构建一个监听器类,该类需要实现NotificationListener接口并实现handleNotification方法
    public static class ClientListener implements NotificationListener {
        @Override
        public void handleNotification(Notification notification, Object handback) {
            echo("\nReceived notification:");
            echo("\tClassName: " + notification.getClass().getName());
            echo("\tSource: " + notification.getSource());
            echo("\tType: " + notification.getType());
            echo("\tMessage: " + notification.getMessage());
            //如果通知类型是AttributeChangeNotification,那么就获取一些和属性有关的信息
            if (notification instanceof AttributeChangeNotification) {
                AttributeChangeNotification acn = (AttributeChangeNotification) notification;
                echo("\tAttributeName: " + acn.getAttributeName());
                echo("\tAttributeType: " + acn.getAttributeType());
                echo("\tNewValue: " + acn.getNewValue());
                echo("\tOldValue: " + acn.getOldValue());
            }
        }

        public static void main(String[] args) throws IOException, MalformedObjectNameException, InstanceNotFoundException, InterruptedException {
            echo("\nCreate an RMI connector client and " +
                    "connect it to the RMI connector server");
            //构造并获取RMI连接
            JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi");
            JMXConnector jmxc = JMXConnectorFactory.connect(url, null);

            //获取MBeanServer的连接
            MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
            waitForEnterPressed();

            echo("\nMBeanServer default domain = " + mbsc.getDefaultDomain());
            echo("\nMBean count = " + mbsc.getMBeanCount());
            echo("\nQuery MBeanServer MBeans:");

            Set<ObjectName> objectNames = mbsc.queryNames(null, null);
            for (ObjectName objectName : objectNames) {
                echo("\tObjectName = " + objectName);
            }
            waitForEnterPressed();

            //创建监听器
            NotificationListener listener = new ClientListener();

            //管理 Hello MBean
            ObjectName mbeanName = new ObjectName("top.yeonon.jmx:type=Hello");
            HelloMBean mbeanProxy = JMX.newMBeanProxy(mbsc, mbeanName, HelloMBean.class, true);
            echo("\nAdd notification listener...");
            mbsc.addNotificationListener(mbeanName, listener, null, null);

            echo("\nCacheSize = " + mbeanProxy.getCacheSize());
            mbeanProxy.setCacheSize(150);
            echo("\nWaiting for notification...");
            sleep(2000);
            echo("\nCacheSize = " + mbeanProxy.getCacheSize());
            echo("\nInvoke sayHello() in Hello MBean...");
            mbeanProxy.sayHello();
            echo("\nInvoke add(2, 3) in Hello MBean...");
            mbeanProxy.add(2,3);

            waitForEnterPressed();

            //关闭客户端
            echo("\nClose the connection to the server");
            jmxc.close();
            echo("\nBye! Bye!");
        }
    }

    private static void echo(String msg) {
        System.out.println(msg);
    }

    private static void waitForEnterPressed() {
        try {
            echo("\nPress <Enter> to continue...");
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码不算少,我将分几步来讲解:

  1. 先创建一个通知监听器,需要实现NotificationListener并实现handleNotification方法,可以在该方法里对消息进行处理,在演示代码中只是将消息的各项信息列出来而已。

  2. 创建一个RMI连接,即如下两行代码:

    //构造一个JMXServiceURL,这里使用的是RMI的连接方式,所以要按照RMI的URL格式,端口是9999,
    JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:9999/jmxrmi");
    //连接远程的JMX Agent
    JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
    
  3. 获取MBeanServer连接,使用jmxc.getMBeanServerConnection()方法来获取,下面的操作大多数都是基于该MBeanServer连接。

  4. 如果有需要的话,可以MBeanServerConnection里取出各种信息,例如MBean的数量以及具体的MBean的名字(使用MBeanServerConnection.queryNames)。

  5. 获取想要管理的MBean代理(这涉及到了代理模式)。使用JMX.newMBeanProxy()方法来生成一个MBean的带来,之后就可以使用该MBean代理对具体的MBean进行操作了(例如修改CacheSize等)。

  6. 如果需要接收通知,就使用MBeanServerConnection.addNotificationListener()方法为特定的MBean绑定一个监听器。

  7. 关闭客户端可以使用JMXConnector.close()方法。

在运行程序之前,需要先运行JMX Agent程序,由于这里是使用RMI进行远程连接,并且连接的端口号是9999,如果直接不再参数启动的话,客户端将无法连接到JMX Agent,需要添加的参数有三个,分别是:

  • -Dcom.sun.management.jmxremote.port=9999,即开放的接口是9999。
  • -Dcom.sun.management.jmxremote.authenticate=false,false表示不需要用户认证。
  • -Dcom.sun.management.jmxremote.ssl=false,false表示不适用SSL安全连接。

加入参数启动之后,就可以尝试启动客户端了,其输出大致如下:


Create an RMI connector client and connect it to the RMI connector server

Press <Enter> to continue...

#我按个回车

MBeanServer default domain = DefaultDomain

#远端的系统中一共有23个MBean
MBean count = 23

#23个MBean的名字
Query MBeanServer MBeans:
    ObjectName = java.lang:type=MemoryPool,name=Metaspace
    ObjectName = java.lang:type=MemoryPool,name=PS Old Gen
    ObjectName = java.lang:type=GarbageCollector,name=PS Scavenge
    ObjectName = java.lang:type=MemoryPool,name=PS Eden Space
    ObjectName = JMImplementation:type=MBeanServerDelegate
    ObjectName = java.lang:type=Runtime
    ObjectName = java.lang:type=Threading
    ObjectName = java.lang:type=OperatingSystem
    ObjectName = java.lang:type=MemoryPool,name=Code Cache
    ObjectName = java.nio:type=BufferPool,name=direct
    ObjectName = java.lang:type=Compilation
    ObjectName = top.yeonon.jmx:type=Hello
    ObjectName = java.lang:type=MemoryManager,name=CodeCacheManager
    ObjectName = java.lang:type=MemoryPool,name=Compressed Class Space
    ObjectName = java.lang:type=Memory
    ObjectName = java.nio:type=BufferPool,name=mapped
    ObjectName = java.util.logging:type=Logging
    ObjectName = java.lang:type=MemoryPool,name=PS Survivor Space
    ObjectName = java.lang:type=ClassLoading
    ObjectName = java.lang:type=MemoryManager,name=Metaspace Manager
    ObjectName = com.sun.management:type=DiagnosticCommand
    ObjectName = java.lang:type=GarbageCollector,name=PS MarkSweep
    ObjectName = com.sun.management:type=HotSpotDiagnostic

Press <Enter> to continue...

#我按个回车

Add notification listener...

CacheSize = 100

Waiting for notification...

#监听器收到了通知
Received notification:
    ClassName: javax.management.AttributeChangeNotification
    Source: top.yeonon.jmx:type=Hello
    Type: jmx.attribute.change
    Message: Cache Size changed
    AttributeName: CacheSize
    AttributeType: int
    NewValue: 150
    OldValue: 100


CacheSize = 150

#调用了sayHello()方法
Invoke sayHello() in Hello MBean...

#调用了add()方法
Invoke add(2, 3) in Hello MBean...

Press <Enter> to continue...
#回车

Close the connection to the server

Bye! Bye!

好了,打完收工!

5 小结

本文简单介绍了JMX的三大组成部分,MBean,JMX Agent,Client,但只使用到了JMX的部分功能,其实JMX远不止如此,还有很多更加高级的功能,例如扩展JMX等,希望本文能对读者有一些帮助。

如果文章一些地方说的有问题,望指正。因为我也是初学JMX,算是边学边写的。

Ps: 本文的样例代码都是来自Oracle JMX Tutorial,下面的参考资料中给出了链接。

6 参考资料

Oracle JMX Tutorial

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 简介 全称Java Management Extensions,从Java5.0开始引入到标准Java技术平台中。...
    0d1b415a365b阅读 2,087评论 0 4
  • 现实企业级 Java 开发中,有时候我们会碰到下面这些问题:OutOfMemoryError,内存不足内存泄露线程...
    达微阅读 5,620评论 0 9
  • 今天终于实现自己本周业绩小目标了,麻D,终于可以豪一把了。提到小目标,你脑中一定浮现年前刷屏朋友圈的王老板说的一句...
    黑眼土匪阅读 564评论 0 2
  • 静候一朵花开,轻嗅一株野菊,撷取一段慵懒的岁月。 我想象着,有一日能挽着父母的手臂,带着他们拜访这所城市,看遍那些...
    厘盖阅读 178评论 0 3
  • 十九大结束了之后校门口的摊位都回来了终于吃到了心心念念的烤冷面。还有香香的红豆粥,又欠我萧萧一杯粥的人情,为我们的...
    冷康康阅读 888评论 0 2