🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥
文章目录
- 一、背景
- 二、JConsole的启动与连接
- 2.1 JConsole的启动
- 2.2 进程连接
- 2.2.1 本地进程连接
- 2.2.2 远程进程连接
- 三、JConsole的界面介绍与实战
- 3.1 概览
- 3.2 内存监控
- 3.3 线程监控
- 3.4 类
- 3.5 vm概要
- 3.6 MBean
- 四、作用
- 五、总结
一、背景
我们在开发java项目时不可避免会遇到一些问题,比如遇到了死锁及一些其他的性能问题,那么发生这些问题该如何去排查呢?之前在公司可能或多或少会去使用它,但是没有专门去记录这个过程,而为了更加全面地认识这个工具,专门写下此篇文章记录一下。在此,也希望本篇文章对您有所帮助。
二、JConsole的启动与连接
2.1 JConsole的启动
- 点击JDK/bin 目录下面的“jconsole.exe”即可启动
- 然后会自动搜索本机运行的所有虚拟机进程
- 选择其中一个进程可开始进行监控
2.2 进程连接
JConsole有两种连接方式,一种是连接本地的进程,一种是连接远程的程序。
2.2.1 本地进程连接
JConsole的本地连接是通过Java的Attach API实现的,Attach API是Java提供的一种机制,允许一个Java进程动态连接到另一个运行中的Java进程(JVM),然后执行一些操作,比如加载代理、获取运行时信息等。
注意:Attach API是Java5引入的功能,位于com.sun.tools.attach包中,它的主要类包括
VirtualMachine
(表示一个JVM进程)、VirtualMachineDescriptor
(描述一个JVM进程的信息)。
本地进程连接流程在JConsole中被简化成了直接选择待attach的目标JVM进程ID。但是其背后进行本地进程连接的完整过程可以大致分如下几个步骤:
获取本地JVM进程列表:
JConsole启动时会调用VirtualMachine.list()方法,获取当前机器上所有运行的JVM进程列表,每个JVM进程由一个VirtualMachineDescriptor 对象表示,包含进程ID和显示名称等信息。选择目标JVM进程:
JConsole 会展示获取到的 JVM 进程列表,用户可以选择一个进程进行连接。连接到目标JVM:
调用 VirtualMachine.attach(pid) 方法,连接到目标 JVM 进程,连接成功后,返回一个 VirtualMachine 对象,表示与目标 JVM 的连接。加载JMX代理:
通过 VirtualMachine 对象,JConsole 可以加载 JMX 代理(management-agent.jar),JMX 代理会启动目标 JVM 的 JMX 服务,并返回 JMX 连接地址。建立JMX连接:
JConsole 使用 JMX 连接地址,通过 JMX 连接到目标 JVM 的 MBeanServer,连接成功后,JConsole 可以访问目标 JVM 的 MBean,获取运行时数据。
下面有一个死锁的Demo案例,这个案例是本文全文会使用的案例。我们先把它运行起来之后,分别通过启动JConsole工具、手写Java代码去进行一个本地进程连接。
package com.wasteland.blogsourcecode.jconsole;
/**
* @author wasteland
* @create 2025-02-12
*/
public class DeadLockDemo {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
int result = cacluate(4, 5);
System.out.println("thread1 calculate 4 + 5 result is : " + result);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "thread1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
int result = cacluate(1, 3);
System.out.println("thread2 calculate 1 + 3 result is : " + result);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "thread2").start();
}
private static int cacluate(int a, int b) {
return a + b;
}
}
运行JConsole工具后,窗口展示如下图,我们可以勾选上要连接的进程如图中运行的死锁Demo的进程ID,勾选后点击连接即可。
使用JConsole工具进行本地进程连接非常简单,那么用Java代码该如何去实现呢?下面是Java语言实现本地进程连接的Demo:
package com.wasteland.blogsourcecode.jconsole;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.List;
/**
* @author wasteland
* @create 2025-03-15
*/
public class AttachExample {
public static void main(String[] args) {
try {
// 获取本地 JVM 进程列表
List<VirtualMachineDescriptor> vmDescriptors = VirtualMachine.list();
String targetPid = "";
for (VirtualMachineDescriptor descriptor : vmDescriptors) {
System.out.println("PID: " + descriptor.id() + ", Name: " + descriptor.displayName());
if ("com.wasteland.blogsourcecode.jconsole.DeadLockDemo".equals(descriptor.displayName())) {
// 找到目标进程,进行连接
targetPid = descriptor.id();
break;
}
}
VirtualMachine vm = VirtualMachine.attach(targetPid);
// 获取 JMX 连接地址
String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
if (connectorAddress == null) {
// 如果 JMX 代理未启动,则加载代理
String agentPath = vm.getSystemProperties().getProperty("java.home") +
"/lib/management-agent.jar";
vm.loadAgent(agentPath);
connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
}
System.out.println("JMX Connector Address: " + connectorAddress);
// 建立 JMX 连接
JMXServiceURL url = new JMXServiceURL(connectorAddress);
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
// 获取 ThreadMXBean
ThreadMXBean threadMXBean = ManagementFactory.newPlatformMXBeanProxy(
connection, ManagementFactory.THREAD_MXBEAN_NAME, ThreadMXBean.class);
// 断开连接
jmxConnector.close();
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码执行效果如下图:
可以看到控制台已经打印了所有的JVM进程,而检测到后可以attach上指定进程,进而去获取JVM内部信息。
2.2.2 远程进程连接
Console 的远程连接是通过 JMX(Java Management Extensions) 实现的。远程连接需要目标 JVM 启用 JMX 远程管理功能,并配置相应的端口和认证信息。以下是JConsole远程进程连接的具体流程:
目标JVM启用JMX远程管理:
在启动目标 Java 应用程序时,需要通过 JVM 参数启用 JMX 远程管理功能。启动JConsole
选择远程连接:
在 JConsole 的启动界面中,选择 远程连接(Remote Process),可以选择输入<目标主机IP>:<JMX端口号>或者service:jmx:rmi:///jndi/rmi://<目标主机IP>:<JMX端口号>/jmxrmi
认证(可选):
如果目标 JVM 启用了认证,JConsole 会弹出认证对话框,要求输入用户名和密码。建立连接:
JConsole 通过 JMX 连接到目标 JVM 的 MBeanServer,连接成功后,JConsole 可以访问目标 JVM 的 MBean,获取运行时数据。
这里由于没有云服务器,就把本机服务器当作远程服务器,然后选择远程进程方式进行连接测试了。
首先按照上面介绍的步骤,我们需要配置一些JVM启动参数:
/ 启用 JMX 远程管理
-Dcom.sun.management.jmxremote
/ 指定 JMX 连接的端口号
-Dcom.sun.management.jmxremote.port=12345
/ 禁用认证
-Dcom.sun.management.jmxremote.authenticate=false
/ 禁用 SSL 加密(仅用于测试环境,生产环境建议启用 SSL)。
-Dcom.sun.management.jmxremote.ssl=false
/ 如果需要启用认证和 SSL,可以做以下配置:
/-Dcom.sun.management.jmxremote.authenticate=true
/-Dcom.sun.management.jmxremote.ssl=true
/-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password
/-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access
在启动程序时,JVM参数配置里配置如上参数:
然后启动JConsole,输入<目标主机IP>:<JMX端口号>或者service:jmx:rmi:///jndi/rmi://<目标主机IP>:<JMX端口号>/jmxrmi都可以进行远程连接,因为这里是选的本机当远程主机,因此主机IP是127.0.0.1,JMX端口号是上面配置的12345。连接上后显示如下图所示 :
它在JConsole的远程连接过程如上文中所介绍,那么它背后是怎么实现的呢?它是通过JMX Connector
和RMI
实现的。JMX 远程连接是通过 JMX Connector 实现,JMX Connector 是一个客户端-服务器模型,客户端(如 JConsole)通过 JMX Connector 连接到服务器(目标 JVM)。JMX 远程连接底层使用 RMI(远程方法调用) 进行通信。目标 JVM 会启动一个 RMI 注册表,并将 JMX Connector Server 绑定到 RMI 注册表中,客户端通过 RMI 查找 JMX Connector Server,并建立连接。
使用JConsole工具进行远程进程连接非常简单,那么用Java代码该如何去实现呢?下面是Java语言实现本地进程连接的Demo:
package com.wasteland.blogsourcecode.jconsole;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
/**
* @author wasteland
* @create 2025-03-15
*/
public class JMXRemoteExample {
public static void main(String[] args) {
try {
// JMX 服务地址
String jmxUrl = "service:jmx:rmi:///jndi/rmi://127.0.0.1:12345/jmxrmi";
JMXServiceURL url = new JMXServiceURL(jmxUrl);
// 创建 JMX 连接
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
// 获取运行时信息
System.out.println("MBean Count: " + connection.getMBeanCount());
// 关闭连接
jmxConnector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码执行效果如下图:
在了解过这两种不同的连接方式之后,不难发现本质都是根据JMX服务地址建立的连接,JMX(Java Management Extensions)是JConsole工具实现的根本原理。
三、JConsole的界面介绍与实战
在选择一个进程并点击连接后,可以看到界面展示如下图,JConsole界面主要分为六大部分,概览,内存,线程,类,VM和MBean。
3.1 概览
概览展示了堆内存使用量,线程,类和CPU占用率这四大内容。
3.2 内存监控
根据上图中我们可以监控堆内存和非堆内存的使用量,具体又细化到Old Gen、Eden Space、Survivor Space、Metaspace、Code Cache以及Compressed Class Space几大存储区域。由于本图中项目是前文中的死锁代码,其开发环境是JDK8,而JDK8默认垃圾收集器是Parallel Scavenge 收集器,因此可以看到图中它的年轻代和老年代名称具体为:PS Old Gen、PS Eden Space以及PS Survivor Space。
各区域存储内容总结如下:
内存区域 | 存储内容 |
---|---|
PS Old Gen | 生命周期较长的对象或者说大对象 |
PS Eden Space | 对象生命周期很短的对象 |
PS Survivor Space | 对象生命周期相对较长的对象 |
Metaspace | 类的结构信息、类的字节码、类的注解信息、类的静态变量等 |
Code Cache | JIT编译后的方法代码、运行时生成的适配器及方法句柄等代码、JVM内部使用的解释器及编译器本身代码 |
Compressed Class Space | 存放压缩后的类原数据:类的结构信息、类的静态变量、类的字节码信息 |
点击执行GC按钮即可进行垃圾回收。通过两块内存监控图表也能看出:点击执行GC按钮后,会将年轻代里的对象放到老年代中。
3.3 线程监控
线程Tab中列出了程序目前正在运行的线程,如果点击具体的线程还可以看到线程中的堆栈跟踪和线程状态统计,非常有用。
因为attach上的是前文开头介绍的死锁Demo,点击检测死锁按钮后它可以直接帮我们定位到代码发生死锁的位置:
有朋友会好奇它这是怎么做到的呢?😱我们要记住其背后的原理:通过建立JMX连接后使用暴露出的MBean获取到JVM内部运行状态。那我们自己用Java代码可以实现这个检测死锁的过程吗?毫无疑问是可以的,我们只需要在2.2.1章节本地连接进程Demo上继续前进一步即可完成,Java代码实现过程如下:
package com.wasteland.blogsourcecode.jconsole;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.List;
/**
* @author wasteland
* @create 2025-03-15
*/
public class AttachExample {
public static void main(String[] args) {
try {
// 获取本地 JVM 进程列表
List<VirtualMachineDescriptor> vmDescriptors = VirtualMachine.list();
String targetPid = "";
for (VirtualMachineDescriptor descriptor : vmDescriptors) {
System.out.println("PID: " + descriptor.id() + ", Name: " + descriptor.displayName());
if ("com.wasteland.blogsourcecode.jconsole.DeadLockDemo".equals(descriptor.displayName())) {
// 找到目标进程,进行连接
targetPid = descriptor.id();
break;
}
}
VirtualMachine vm = VirtualMachine.attach(targetPid);
// 获取 JMX 连接地址
String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
if (connectorAddress == null) {
// 如果 JMX 代理未启动,则加载代理
String agentPath = vm.getSystemProperties().getProperty("java.home") +
"/lib/management-agent.jar";
vm.loadAgent(agentPath);
connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
}
System.out.println("JMX Connector Address: " + connectorAddress);
// 建立 JMX 连接
JMXServiceURL url = new JMXServiceURL(connectorAddress);
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
// 获取 ThreadMXBean
ThreadMXBean threadMXBean = ManagementFactory.newPlatformMXBeanProxy(
connection, ManagementFactory.THREAD_MXBEAN_NAME, ThreadMXBean.class);
// 检测死锁
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.out.println("Deadlock detected!");
for (long threadId : deadlockedThreads) {
// 获取死锁的线程信息
// 为了确保堆栈跟踪信息被打印完全不被截断,这里最好指定线程堆栈跟踪的深度
ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, Integer.MAX_VALUE);
System.out.println("Deadlocked Thread: " + threadInfo.getThreadName());
System.out.println("Stack Trace:");
for (StackTraceElement element : threadInfo.getStackTrace()) {
System.out.println("\t" + element.toString());
}
}
} else {
System.out.println("No deadlock detected.");
}
// 断开连接
jmxConnector.close();
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码执行效果如下图,能够看出和用JConsole工具检测出的死锁信息基本一致:
3.4 类
在类的这个tab下展示就相对来说比较简单了,仅显示了加载的类的总数。
3.5 vm概要
vm信息会展示虚拟机相关的一些参数比如虚拟机版本、JIT编译器及垃圾收集器等。
3.6 MBean
JConsole中最后一个tab展示的就是MBean,需要知道的是前文介绍的内存、类及VM概要信息其实从根本上来说都是基于MBean获取到的。
MBean(Managed Bean) 是 Java 管理扩展(JMX)的核心组件,用于暴露和管理 Java 应用程序的内部状态和操作。
MBean 提供了一种标准化的方式,允许外部工具(如 JConsole)监控和管理应用程序的运行状态。
MBean的作用如下:
暴露应用程序的内部状态:
MBean 可以暴露应用程序的各种运行时数据,如内存使用情况、线程状态、类加载信息、GC 信息等。例如,java.lang:type=Memory MBean 提供了 JVM 内存使用情况的信息。提供管理工作:
MBean 可以定义一些管理操作,允许外部工具动态修改应用程序的配置或执行特定任务。例如,java.lang:type=Threading MBean 提供了查找死锁线程、重置线程 CPU 时间等操作。标准化接口:
MBean 遵循 JMX 规范,提供标准化的接口,使得不同的监控工具(如 JConsole、VisualVM)可以统一访问和管理应用程序。
有朋友读到这里可能又会有疑问了,上面JConsole工具界面里展示的是默认已经实现的MBean,那么用户是否可以自定义MBean呢?当然是可以的,开发者不仅可以自定义MBean,并且在JConsole的工具的MBean目录下可以找到自定义的MBean。本文接下来会自己实现一个MBean,并且注册到MBean服务器中,案例还是前文介绍的死锁Demo,自定义MBean的作用是监测死锁Demo案例中的calculate这个方法的执行次数,具体结构分为3个类:MBean接口、MBean接口实现类、DeadLockDemo类。
MBean接口:
package com.wasteland.blogsourcecode.jconsole;
/**
* @author wasteland
* @create 2025-03-15
*/
public interface MethodMonitorMBean {
// 获取方法执行次数
int getInvocationCount();
// 重置方法执行次数统计
void resetInvocationCount();
}
MBean接口实现类:
package com.wasteland.blogsourcecode.jconsole;
/**
* @author wasteland
* @create 2025-03-15
*/
public class MethodMonitor implements MethodMonitorMBean {
private int invocationCount = 0;
@Override
public int getInvocationCount() {
return invocationCount;
}
@Override
public void resetInvocationCount() {
invocationCount = 0;
}
public void monitoredMethod() {
invocationCount++;
System.out.println("Method invoked. Current count: " + invocationCount);
}
}
DeadLockDemo类:
package com.wasteland.blogsourcecode.jconsole;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
/**
* @author wasteland
* @create 2025-03-15
*/
public class DeadLockDemo {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
private static volatile MethodMonitor methodMonitor;
public static MethodMonitor getInstance() {
// 判断对象是否已经实例化过,没有实例化才进入加锁代码
if (methodMonitor == null) {
// 类对象加锁
synchronized (DeadLockDemo.class) {
if (methodMonitor == null) { // 可以避免重复创建对象
methodMonitor = new MethodMonitor();
}
}
}
return methodMonitor;
}
public static void main(String[] args) throws Exception {
// 获取 MBean 服务器
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
// 创建 MBean 实例
MethodMonitor methodMonitor = DeadLockDemo.getInstance();
// 注册 MBean
ObjectName objectName = new ObjectName("com.wasteland.blogsourcecode.jconsole:type=MethodMonitor");
mBeanServer.registerMBean(methodMonitor, objectName);
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
int result = cacluate(4, 5);
System.out.println("thread2 calculate 4 + 5 result is : " + result);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "thread1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
int result = cacluate(1, 3);
System.out.println("thread2 calculate 1 + 3 result is : " + result);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "thread2").start();
}
private static int cacluate(int a, int b) {
methodMonitor.monitoredMethod();
return a + b;
}
}
程序启动后,利用JConsole工具attach上这个死锁进程,连接成功后点击到MBean的Tab下,可以看到我们自己注册的MethodMonitor,其定义的带返回值InvocationCount方法在属性目录下,无返回值的resetInovationCount方法在操作目录下:
四、作用
根据前文介绍,不难发现JConsole工具有如下几大作用:
- 系统资源实时监控
- CPU使用率监控、内存使用情况监控、线程状态监控及类加载情况监控
- JVM参数配置
- JConsole可以显示当前JVM各种参数配置 ,还可以根据程序实际运行情况动态调整一些JVM参数
- 性能分析与故障诊断
- 当系统出现卡顿时,可以查看CPU、内存和线程状态,进而判断是资源耗尽还是线程阻塞导致,还可以结合jstack及jmap命令获得更加详细信息
五、总结
整体来说,JConsole是一个比较简单但是也很实用的profile工具,能够满足基本监控与性能分析的需求。在实际业务开发中,业务功能实现很重要,但是代码的性能和效率也同样重要,希望大家能够在编写代码之余多多关注。