JMX简单使用

jmx

JMX(Java Management Extensions)是Java平台上为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

结构

191023-jmx-mbean

JMX Jconsole以及Web Browser属于应用层,我们可以不用关注:

  • 分布层(Distributed layer):即图中RMI,HTTP/SOAP,HTTP/HTML,SNMP,这层通常被封装成适配器(Adapter),将JMX信息封装成调用接口提供给外层
  • 代理层(Agent layer):即图中的MBeanServer,负责MBeans的注册管理
  • 仪表层(Instrumentation layer):即图最下方的MBeans,每个MBean都是一个被管理的资源,通过对外暴露的方法和属性,实现被上层获取和调用

远程连接

如果要启用远程连接,需要在启动应用时添加以下参数:

1
2
3
4
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8008 // jmx远程端口
-Dcom.sun.management.jmxremote.authenticate=false // 不启用认证
-Dcom.sun.management.jmxremote.ssl=false // 不使用SSL连接

连接Tomcat配置

可以直接修改catalina.sh,也可以修改setenv.sh(推荐):

摘自catalina.sh

Do not set the variables in this script. Instead put them into a script setenv.sh in CATALINA_BASE/bin to keep your customizations separate.

无需认证配置

1
2
3
4
CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.port=7431 // jmx远程端口
-Dcom.sun.management.jmxremote.authenticate=false // 不启用认证
-Dcom.sun.management.jmxremote.ssl=false // 不使用SSL连接
-Djava.rmi.server.hostname=192.168.80.13" // 本机IP

认证配置

1
2
3
4
5
6
CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.port=7431 // jmx远程端口
-Dcom.sun.management.jmxremote.authenticate=true // 启用认证
-Dcom.sun.management.jmxremote.ssl=false // 不使用SSL连接
-Djava.rmi.server.hostname=192.168.80.13 // 本机IP
-Dcom.sun.management.jmxremote.acccess.file=/home/vv-dev/jconsole/jmxremote.access // 授权用户配置文件路径
-Dcom.sun.management.jmxremote.password.file=/home/vv-dev/jconsole/jmxremote.password" // 授权用户密码配置文件路径

jmxremote.access&jmxremote.password两个文件模板可以在../jdk/jre/lib/management路径下找到

此处需要对这两个文件变更权限:

1
$ chmod 600 jmxremote.*

jmxremote.access文件中配置了用户名,以及对应的权限:

1
2
3
4
monitorRole   readonly  // 只读,监控权限配置这个即可
controlRole readwrite \ // 读写权限
create javax.management.monitor.*,javax.management.timer.* \
unregister

jmxremote.password文件中配置了用户名,以及对应的密码:

1
2
monitorRole  QED
controlRole R&D

配置完成后需要重启服务,通过jconsole,使用配置的用户名(monitorRole)和密码(QED)即可连接服务

获取示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JmxSimpleTest {

public static void main(String[] args) throws Exception {
String host = "192.168.80.13:7431"; // 服务器所在IP及配置的远程端口
String jmxURL = "service:jmx:rmi:///jndi/rmi://" + host + "/jmxrmi";
JMXServiceURL serviceURL = new JMXServiceURL(jmxURL);

// ------ 用户认证配置 ------
// Map<String, String[]> map = new HashMap<>();
// String[] credentials = new String[] { "monitorRole", "QED" };
// map.put("jmx.remote.credentials", credentials);
// JMXConnector jMXConnector = JMXConnectorFactory.connect(serviceURL, map);

try (JMXConnector jMXConnector = JMXConnectorFactory.connect(serviceURL)) {
MBeanServerConnection mbsc = jMXConnector.getMBeanServerConnection();
// 获取类加载信息
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getPlatformMXBean(mbsc, ClassLoadingMXBean.class);
System.out.println("LoadedClassCount:" + classLoadingMXBean.getLoadedClassCount());
System.out.println("TotalLoadedClassCount:" + classLoadingMXBean.getTotalLoadedClassCount());
System.out.println("UnloadedClassCount:" + classLoadingMXBean.getUnloadedClassCount());
}
}
}

ManagementFactory.java中定义了一些我们可以直接获取到的jvm信息的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// jvm类加载数量,卸载数量,对应`ClassLoadingMXBean`
public final static String CLASS_LOADING_MXBEAN_NAME =
"java.lang:type=ClassLoading";

// jvm编译信息,对应`CompilationMXBean`
public final static String COMPILATION_MXBEAN_NAME =
"java.lang:type=Compilation";

// jvm内存信息,对应`MemoryMXBean`
public final static String MEMORY_MXBEAN_NAME =
"java.lang:type=Memory";

// 操作系统信息,对应`OperatingSystemMXBean`
public final static String OPERATING_SYSTEM_MXBEAN_NAME =
"java.lang:type=OperatingSystem";

// jvm运行信息,对应`RuntimeMXBean`
public final static String RUNTIME_MXBEAN_NAME =
"java.lang:type=Runtime";

// jvm线程信息,对应`ThreadMXBean`
public final static String THREAD_MXBEAN_NAME =
"java.lang:type=Threading";

// GC信息,对应`GarbageCollectorMXBean`,获取到的是列表
public final static String GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE =
"java.lang:type=GarbageCollector";

// 内存管理信息,对应`MemoryManagerMXBean`,获取到的是列表
public final static String MEMORY_MANAGER_MXBEAN_DOMAIN_TYPE=
"java.lang:type=MemoryManager";

// 内存池信息,对应`MemoryPoolMXBean`,获取到的是列表
public final static String MEMORY_POOL_MXBEAN_DOMAIN_TYPE=
"java.lang:type=MemoryPool";

对比jconsole,通过上面这种方式获取到的信息有限,例如OperatingSystem只能获取到处理器数量等基本信息,无法获取到像是文件描述符数,交换空间大小等信息:

191023-jmx-OperatingSystem

好在jmx还提供了另外一种方法MBeanServerConnection.getAttributes()

1
2
3
4
5
ObjectName objectName = new ObjectName("java.lang:type=OperatingSystem");
AttributeList attributeList = mbsc.getAttributes(objectName, new String[] { "MaxFileDescriptorCount", "TotalSwapSpaceSize" });
for (Attribute attribute : attributeList.asList()) {
System.out.println(attribute.getName() + ": " + attribute.getValue());
}

通过这种方法,我们也可以获取tomcat相关信息:

1
2
3
4
5
ObjectName objectName = new ObjectName("Catalina:name=\"http-nio2-20041\",type=GlobalRequestProcessor"); // 此处的20041为tomcat运行端口,在server.xml配置
MBeanInfo mBeanInfo = mbsc.getMBeanInfo(objectName);
for (MBeanAttributeInfo attribute : mBeanInfo.getAttributes()) {
System.out.println(attribute.getName() + ": " + mbsc.getAttribute(objectName, attribute.getName()));
}

创建自定义MBean

假如现在要定义一个计数器Counter,按照惯例,Counter对应的MBean需要以MBean结尾:

1
2
3
4
5
6
7
8
public interface CounterMBean {

long getCount();

void setCount(long count);

void incr();
}

CounterMBean中定义的方法,都是可以提供给远程调用的,现在做下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Counter implements CounterMBean {

private AtomicLong counter = new AtomicLong(0);

@Override
public long getCount() {
return counter.get();
}

@Override
public void setCount(long count) {
counter.set(count);
}

@Override
public void incr() {
counter.incrementAndGet();
}
}

接下来我们需要在MbeanServer中注册CounterMBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CounterJmxRegisterTest {
public static void main(String[] args) throws Exception {

Counter counter = new Counter();

// 获取MbeanServer
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
// 定义一个名称
ObjectName objectName = new ObjectName("test.jmx:name=counter");
// 注册mbean
server.registerMBean(counter, objectName);

// 定义远程端口号
int rmiPort = 1099;
LocateRegistry.createRegistry(rmiPort);
// URL路径的结尾可以随意指定,但如果需要用jconsole来进行连接,则必须使用jmxrmi
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + rmiPort + "/jmxrmi");
JMXConnectorServer jcs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
jcs.start();

while (true) {
counter.incr();
System.out.println(counter.getCount());
Thread.sleep(5000);
}
}
}

ObjectName定义规则为“域:键值对(可以多个)”,的值可以任意,示例中为test.xyz:后为多组键值对(在jconsole中以嵌套文件夹体现),如a=abc或者a=a,b=b

运行后使用jconsole连接:

191023-jmx-mbean-register

可以看到CounterMBean已经注册了,并且提供了一个可执行的方法incr,以及可修改的属性Count,因为我们对Count实现了get/set方法,所以显示的的可读/可写都是true

修改MBean属性值和调用方法

除了获取MBean的属性值外,还可以修改MBean的属性值和调用接口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CounterJmxTest {

public static void main(String[] args) throws Exception {
String jmxURL = "service:jmx:rmi:///jndi/rmi://127.0.0.1:1099/jmxrmi";
JMXServiceURL serviceURL = new JMXServiceURL(jmxURL);
try (JMXConnector jMXConnector = JMXConnectorFactory.connect(serviceURL)) {
MBeanServerConnection mbsc = jMXConnector.getMBeanServerConnection();

ObjectName objectName = new ObjectName("test.jmx:name=counter");
// 获取属性值
long countValue = (long) mbsc.getAttribute(objectName, "Count");
System.out.println("CountValue:" + countValue);

// 设置属性值
mbsc.setAttribute(objectName, new Attribute("Count", 1024));
countValue = (long) mbsc.getAttribute(objectName, "Count");
System.out.println("CountValue:" + countValue);

// 调用方法
mbsc.invoke(objectName, "incr", null, null);
mbsc.invoke(objectName, "incr", null, null);
mbsc.invoke(objectName, "incr", null, null);

countValue = (long) mbsc.getAttribute(objectName, "Count");
System.out.println("CountValue:" + countValue);
}
}
}

通知(Notification)

Notification起到了在MBean之间沟通桥梁的作用,Notification模型和Java Event模型类似,将一些重要的信息,状态的转变,数据的变更传递给Notification Listener,以便资源的管理。Notification由四部分组成:

1
2
3
4
Notification:通用来说就是需要发送的消息,可以被直接使用,也可以根据传递的事件的需要而被扩展。
NotificationListener:用于监听广播出来的通知信息
NotificationFilter:为监听者提供了一个过滤通知的过滤器,过滤掉不需要的通知
NotificationBroadcaster:通知发送者需实现此接口,发送需要传递的信息

修改Counter,添加一个通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Counter extends NotificationBroadcasterSupport implements CounterMBean {

private AtomicLong counter = new AtomicLong(0);

private long sequenceNumber = 1;

@Override
public long getCount() {
return counter.get();
}

@Override
public void setCount(long count) {
// 可以使用AttributeChangeNotification:属性变化通知
Notification n = new Notification(// 创建一个通知
"setCount", // 名称
this, // 发送者
sequenceNumber++, // 序列号,可以设置任意数值
System.currentTimeMillis(), // 发出时间
"Count is setted, current: " + count);// 发出信息的消息文本
sendNotification(n);

counter.set(count);
}

@Override
public void incr() {
counter.incrementAndGet();
}
}

创建监听器:

1
2
3
4
5
6
7
8
9
public class CounterListener implements NotificationListener {

@Override
public void handleNotification(Notification notification, Object handback) {
// if (notification instanceof AttributeChangeNotification) {
// AttributeChangeNotification attributeChangeNotification = (AttributeChangeNotification) notification;
// }
System.out.println(notification.getMessage());
}

在注册MBean后,注册监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CounterJmxRegisterTest {
public static void main(String[] args) throws Exception {

Counter counter = new Counter();

// 获取MbeanServer
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
// 定义一个名称
ObjectName objectName = new ObjectName("test.jmx:name=counter");
// 注册mbean
server.registerMBean(counter, objectName);

// 添加监听器
server.addNotificationListener(objectName, new CounterListener(), null, counter);

// 定义远程端口号
int rmiPort = 1099;
LocateRegistry.createRegistry(rmiPort);
// URL路径的结尾可以随意指定,但如果需要用jconsole来进行连接,则必须使用jmxrmi
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + rmiPort + "/jmxrmi");
JMXConnectorServer jcs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
jcs.start();

while (true) {
counter.incr();
System.out.println(counter.getCount());
Thread.sleep(5000);
}
}
}

当调用Counter.setCount时,控制台输出:

1
2
3
1
Count is setted, current: 1024
1028

参考

JMX远程监控JVM
Deep Dive into Java Management Extensions (JMX)
从零开始玩转JMX
JMX超详细解读
JMX monitoring + Java custom metrics.