
在使用工具之前,我们必须先搞懂:这些工具是如何与JVM交互,实现性能数据采集的?核心答案是 JMX(Java Management Extensions),即Java管理扩展。JMX是Java平台提供的一套用于监控和管理应用程序、设备、系统等资源的标准API,JVM本身已经实现了JMX的核心组件,暴露了大量可监控的MBean(管理Bean),包含内存、线程、类加载、GC等关键性能指标。
JConsole与JVisualVM的底层工作流程完全基于JMX,其核心逻辑可概括为:
很多人会混淆JConsole与JVisualVM,其实二者的定位有明确区别,适用场景也各有侧重:
简单总结:JConsole是“日常巡检工具”,JVisualVM是“深度诊断专家”。
两款工具均内置在JDK中,无需额外安装,只要配置好JDK环境即可直接使用。本文所有案例基于 JDK 17(最新LTS版本),建议读者统一环境,避免版本差异导致的问题。
bin目录,双击jconsole.exe(Windows)或jconsole(Linux/Mac)启动JConsole;双击jvisualvm.exe(Windows)或jvisualvm(Linux/Mac)启动JVisualVM;jconsole或jvisualvm,前提是JDK的bin目录已配置到系统环境变量PATH中。JConsole的核心价值在于“快”和“简”,无需复杂配置,即可快速连接目标JVM,获取关键性能数据。本节从连接方式、核心功能、实战技巧三个维度,全面讲解JConsole的使用。
JConsole支持两种连接模式:本地进程连接(适用于开发环境)和远程进程连接(适用于生产/测试环境)。
本地连接是最常用的方式,适用于监控本机运行的Java进程,步骤如下:
远程连接适用于监控服务器上的Java应用,需要先在目标服务器的Java应用启动参数中配置JMX相关参数,步骤如下:
在启动Java应用时,添加以下JVM参数(以Linux环境为例):
java -jar \
-Djava.rmi.server.hostname=192.168.1.100 \ # 服务器IP地址
-Dcom.sun.management.jmxremote \ # 开启JMX远程监控
-Dcom.sun.management.jmxremote.port=8888 \ # JMX监听端口(自定义,需开放防火墙)
-Dcom.sun.management.jmxremote.ssl=false \ # 关闭SSL(生产环境建议开启,需配置证书)
-Dcom.sun.management.jmxremote.authenticate=false \ # 关闭身份验证(生产环境建议开启,配置用户名密码)
demo.jar
生产环境安全配置补充: 如果需要开启身份验证,需额外配置:
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.password.file=jmxremote.password \ # 密码文件路径
-Dcom.sun.management.jmxremote.access.file=jmxremote.access \ # 权限文件路径
其中,jmxremote.access和jmxremote.password文件位于JDK的conf/management目录下,需修改权限(仅所有者可读写):
chmod 600 jmxremote.access jmxremote.password
编辑jmxremote.access添加用户权限(如admin readwrite),编辑jmxremote.password添加用户名密码(如admin 123456)。
192.168.1.100:8888);JConsole的监控界面分为6个核心模块:概述、内存、线程、类、VM概要、MBean,每个模块对应不同的性能监控维度。
概述模块是所有核心指标的“仪表盘”,展示4个关键指标的实时趋势图:
通过概述模块,可快速判断应用的整体运行状态。例如:如果堆内存曲线持续上升且不回落,可能存在内存泄漏;如果CPU使用率长期处于100%,说明存在CPU密集型任务阻塞。
内存模块是排查内存问题的核心,用于监控JVM内存的分配与使用情况,支持查看不同内存区域的详细数据。
JVM的堆内存分为年轻代(Eden+Survivor0+Survivor1)和老年代,年轻代用于存放新创建的对象,老年代用于存放长期存活的对象。当Eden区满时,会触发Minor GC;当老年代满时,会触发Full GC。JConsole通过JMX获取MemoryMXBean和MemoryPoolMXBean的数据,实现对各内存区域的监控。
OutOfMemoryError: Direct buffer memory,需检查NIO相关代码(如ByteBuffer.allocateDirect)的使用是否合理。线程模块用于监控线程的运行状态,是排查线程死锁、线程阻塞的关键工具。
JConsole通过ThreadMXBean获取线程的运行状态数据,线程的状态分为:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)。死锁检测的核心是通过ThreadMXBean.findDeadlockedThreads()方法,识别出互相持有对方所需锁的线程。
下面通过一个可直接运行的案例,演示如何用JConsole排查线程死锁。
package com.jam.demo.jconsole;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Api;
import io.swagger.v3.oas.annotations.Operation;
/**
* 线程死锁演示案例
* @author ken
*/
@RestController
@RequestMapping("/deadlock")
@Api(tags = "线程死锁演示接口")
@Slf4j
publicclass DeadlockDemoController {
// 定义两个锁对象
privatestaticfinal Object LOCK_A = new Object();
privatestaticfinal Object LOCK_B = new Object();
/**
* 触发死锁
*/
@GetMapping("/trigger")
@Operation(summary = "触发线程死锁", description = "启动两个线程,互相持有对方所需的锁,导致死锁")
public String triggerDeadlock() {
// 线程1:先获取LOCK_A,再尝试获取LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
log.info("线程1:已获取LOCK_A,准备获取LOCK_B");
try {
// 模拟业务耗时,让线程2有机会获取LOCK_B
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程1睡眠被中断", e);
Thread.currentThread().interrupt();
}
synchronized (LOCK_B) {
log.info("线程1:已获取LOCK_B");
}
}
}, "Thread-Deadlock-1").start();
// 线程2:先获取LOCK_B,再尝试获取LOCK_A
new Thread(() -> {
synchronized (LOCK_B) {
log.info("线程2:已获取LOCK_B,准备获取LOCK_A");
try {
// 模拟业务耗时,让线程1有机会获取LOCK_A
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程2睡眠被中断", e);
Thread.currentThread().interrupt();
}
synchronized (LOCK_A) {
log.info("线程2:已获取LOCK_A");
}
}
}, "Thread-Deadlock-2").start();
return"已启动两个线程,大概率已触发死锁,请通过JConsole排查";
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>jvm-tuning-demo</artifactId>
<version>1.0.0</version>
<name>jvm-tuning-demo</name>
<description>JVM调优工具实战演示项目</description>
<properties>
<java.version>17</java.version>
<fastjson2.version>2.0.47</fastjson2.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
http://localhost:8080/deadlock/trigger,触发死锁;通过JConsole的死锁检测结果,可明确:Thread-Deadlock-1持有LOCK_A,等待LOCK_B;Thread-Deadlock-2持有LOCK_B,等待LOCK_A。解决方法是统一线程获取锁的顺序(如两个线程都先获取LOCK_A,再获取LOCK_B),修改后的代码如下:
// 线程1:顺序不变,先LOCK_A再LOCK_B
// 线程2:修改顺序,先LOCK_A再LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
log.info("线程2:已获取LOCK_A,准备获取LOCK_B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程2睡眠被中断", e);
Thread.currentThread().interrupt();
}
synchronized (LOCK_B) {
log.info("线程2:已获取LOCK_B");
}
}
}, "Thread-Deadlock-2").start();
修改后,两个线程获取锁的顺序一致,不会再出现死锁。
类模块用于监控类加载和卸载的情况,核心指标包括:
通过ClassLoadingMXBean获取类加载相关数据。JVM的类加载过程分为加载、验证、准备、解析、初始化五个阶段,类加载后会被放入方法区(元空间),只有当类的所有引用被释放,且满足卸载条件时,才会被卸载。
VM概要模块展示目标JVM的基础信息,包括:
该模块的核心价值是快速获取JVM的运行环境和配置信息,用于验证JVM参数是否配置正确(如堆内存大小是否符合预期)。
MBean模块是JConsole的“高级功能”,直接展示JVM暴露的所有MBean,支持查看MBean的属性和调用MBean的方法。
java.util.concurrent.ThreadPoolExecutor相关的MBean,可查看线程池的核心线程数、最大线程数、活跃线程数、任务队列大小等;MemoryMXBean的gc()方法手动触发GC,通过ThreadMXBean的dumpAllThreads()方法导出线程堆栈。对于自定义MBean(如监控业务指标),也可通过该模块查看和管理,实现自定义监控。
JVisualVM是JDK中功能最全面的性能分析工具,不仅包含JConsole的所有监控功能,还提供了采样分析、内存快照、线程快照、GC日志分析、插件扩展等高级功能。本节重点讲解其核心高级功能和实战案例。
JVisualVM的连接方式与JConsole类似,支持本地进程、远程进程、JMX连接、 Attach到进程等多种方式,操作更直观(界面采用树形结构展示所有连接的进程)。
监控模块与JConsole的功能类似,包括内存、线程、类、CPU使用率的实时监控,界面更美观,支持自定义监控指标的展示方式(如折线图、柱状图),同时支持将监控数据导出为CSV格式,便于后续分析。
采样分析是JVisualVM的核心高级功能,用于定位“耗时方法”和“内存占用过高的对象”,无需修改代码,通过采样的方式获取数据,对应用性能影响极小。
CPU采样的核心目的是找出CPU使用率高的方法,步骤如下:
CPU采样基于线程的堆栈快照,工具会定期(如每20ms)获取所有运行线程的堆栈信息,统计每个方法在堆栈中的出现次数,从而判断方法的CPU占用情况。采样间隔越小,结果越精确,但对应用性能的影响越大(一般建议使用默认间隔)。
package com.jam.demo.jvisualvm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Api;
import io.swagger.v3.oas.annotations.Operation;
import java.util.ArrayList;
import java.util.List;
/**
* CPU过高问题演示案例
* @author ken
*/
@RestController
@RequestMapping("/cpu")
@Api(tags = "CPU过高演示接口")
@Slf4j
publicclass HighCpuDemoController {
/**
* 触发CPU过高(无限循环+大量字符串拼接)
*/
@GetMapping("/high")
@Operation(summary = "触发CPU过高", description = "通过无限循环和非高效字符串拼接,导致CPU使用率飙升")
public String highCpu() {
log.info("开始执行CPU过高的任务");
// 无限循环,持续消耗CPU
while (true) {
// 非高效字符串拼接(创建大量临时对象,且消耗CPU)
String str = "";
for (int i = 0; i < 1000; i++) {
str += "cpu-high-" + i;
}
// 模拟业务逻辑,避免代码被编译器优化掉
if (str.length() > 0) {
continue;
}
}
}
/**
* 正常业务方法(对比用)
*/
@GetMapping("/normal")
@Operation(summary = "正常业务方法", description = "普通的列表查询业务,CPU使用率正常")
public List<String> normal() {
List<String> result = new ArrayList<>();
for (int i = 0; i < 100; i++) {
result.add("normal-data-" + i);
}
log.info("正常业务方法执行完成,返回数据量:{}", result.size());
return result;
}
}
http://localhost:8080/cpu/high,触发CPU过高场景;HighCpuDemoController.highCpu()方法的采样次数和总时间均为最高;StringBuilder替代字符串拼接(字符串拼接+会创建大量String对象,且每次拼接都需拷贝字符数组,效率极低),优化后的代码:@GetMapping("/high")
@Operation(summary = "触发CPU过高(优化后)", description = "修复无限循环,使用StringBuilder优化字符串拼接")
public String highCpuOptimized() {
log.info("开始执行优化后的CPU任务");
// 移除无限循环,添加退出条件
int count = 0;
while (count < 1000) {
// 使用StringBuilder优化字符串拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("cpu-high-").append(i);
}
if (sb.length() > 0) {
count++;
}
}
log.info("CPU任务执行完成");
return"CPU任务执行完成";
}
优化后,CPU使用率恢复正常。
内存采样的核心目的是找出内存占用过高的对象,步骤如下:
内存采样适用于快速定位大致问题,而内存快照(Heap Dump)是更精准的内存分析工具,会完整导出JVM堆内存中的所有对象信息,包括对象的数量、大小、引用关系等,适合深度排查内存泄漏问题。
内存快照本质是通过HotSpotDiagnosticMXBean的dumpHeap()方法,将JVM堆内存中的所有对象数据写入文件(.hprof格式),然后工具对该文件进行解析,展示对象的详细信息和引用关系。
内存泄漏的核心原因是对象被GC Roots持有,无法被GC回收。下面通过一个静态集合持有对象引用的案例,演示如何用内存快照排查内存泄漏。
package com.jam.demo.jvisualvm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Api;
import io.swagger.v3.oas.annotations.Operation;
import java.util.ArrayList;
import java.util.List;
/**
* 内存泄漏演示案例(静态集合持有对象引用)
* @author ken
*/
@RestController
@RequestMapping("/memory")
@Api(tags = "内存泄漏演示接口")
@Slf4j
publicclass MemoryLeakDemoController {
// 静态集合(GC Roots之一),持有User对象引用
privatestaticfinal List<User> USER_CACHE = new ArrayList<>();
/**
* 添加用户到静态集合(不释放)
*/
@GetMapping("/addUser")
@Operation(summary = "添加用户到静态缓存", description = "将用户对象添加到静态集合,不进行移除,导致内存泄漏")
public String addUser(String name, Integer age) {
// 每次调用创建新的User对象,添加到静态集合
User user = new User(name, age);
USER_CACHE.add(user);
log.info("添加用户成功,当前缓存用户数:{}", USER_CACHE.size());
return"添加用户成功,当前缓存用户数:" + USER_CACHE.size();
}
/**
* 查看缓存用户数
*/
@GetMapping("/getUserCount")
@Operation(summary = "获取缓存用户数", description = "查看静态集合中的用户数量")
public String getUserCount() {
return"当前缓存用户数:" + USER_CACHE.size();
}
// 内部类User
staticclass User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
// getter/setter省略
}
}
http://localhost:8080/memory/addUser?name=test&age=20,添加多个用户;com.jam.demo.jvisualvm.MemoryLeakDemoController$User类的实例数:若实例数未减少,说明存在内存泄漏;USER_CACHE(静态集合)引用,而静态集合属于GC Roots,导致User实例无法被回收。// 改用WeakReference的List,避免强引用
privatestaticfinal List<WeakReference<User>> USER_CACHE = new ArrayList<>();
/**
* 添加用户到静态集合(优化后,使用弱引用)
*/
@GetMapping("/addUserOptimized")
@Operation(summary = "添加用户到静态缓存(优化后)", description = "使用弱引用持有用户对象,避免内存泄漏")
public String addUserOptimized(String name, Integer age) {
User user = new User(name, age);
// 用WeakReference包裹User对象
WeakReference<User> weakUser = new WeakReference<>(user);
USER_CACHE.add(weakUser);
// 清理已被GC回收的弱引用
USER_CACHE.removeIf(ref -> ref.get() == null);
log.info("添加用户成功,当前缓存用户数(含已回收):{}", USER_CACHE.size());
return"添加用户成功,当前缓存用户数(含已回收):" + USER_CACHE.size();
}
线程快照与JConsole的线程Dump功能类似,但JVisualVM的分析功能更强大,支持:
Object.wait()、LockSupport.park()),判断是否存在线程唤醒机制异常。Visual GC是JVisualVM最实用的插件之一,用于可视化展示GC的全过程,包括各内存区域(Eden、Survivor0、Survivor1、老年代、元空间)的大小变化、GC次数、GC耗时等信息,让GC过程“一目了然”。
-Xmn参数;-Xmx参数。与JConsole类似,JVisualVM也支持远程监控,配置步骤如下:
生产环境优化建议:
本节结合一个真实的电商订单系统场景,综合运用JConsole和JVisualVM,排查并解决实际的性能问题。
电商订单系统的“创建订单”接口,在高并发场景下(如秒杀活动),出现响应缓慢(平均响应时间从50ms飙升至500ms),且系统内存持续增长,偶发OOM错误。
OrderServiceImpl.createOrder()方法的总时间最长,其内部调用的RedisUtil.set()方法采样次数极高。查看RedisUtil.set()方法代码:
/**
* 向Redis设置值(存在性能问题)
* @param key 键
* @param value 值
* @param expire 过期时间(秒)
*/
public void set(String key, Object value, long expire) {
// 问题1:每次都创建新的ObjectMapper对象,序列化效率低
ObjectMapper objectMapper = new ObjectMapper();
try {
String jsonValue = objectMapper.writeValueAsString(value);
// 问题2:未使用Redis连接池,每次都创建新的连接
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.setex(key, expire, jsonValue);
jedis.close();
} catch (JsonProcessingException e) {
log.error("Redis序列化失败", e);
}
}
问题定位:
set()方法都创建新的ObjectMapper对象,序列化效率低,消耗大量CPU;com.alibaba.fastjson2.JSONObject实例数异常多(超过10万个);OrderServiceImpl中的一个静态Map(ORDER_CACHE)持有,用于缓存订单信息,但未设置过期清理机制;ORDER_CACHE的代码:// 静态Map缓存订单信息,无过期清理
private static final Map<String, JSONObject> ORDER_CACHE = new HashMap<>();
/**
* 创建订单时缓存订单信息
*/
public OrderDTO createOrder(OrderCreateDTO createDTO) {
// 业务逻辑:创建订单、扣减库存、生成支付信息...
OrderDTO orderDTO = orderMapper.insertOrder(createDTO);
// 缓存订单信息(无过期清理)
JSONObject orderJson = JSONObject.from(getOrderDetail(orderDTO.getId()));
ORDER_CACHE.put(orderDTO.getId(), orderJson);
return orderDTO;
}
问题定位:静态MapORDER_CACHE持有大量订单JSON对象,无过期清理机制,导致对象无法被GC回收,内存持续增长,最终触发OOM。
ObjectMapper对象(改为单例);优化后的RedisUtil:
package com.jam.demo.util;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类(优化后)
* @author ken
*/
@Component
@Slf4j
publicclass RedisUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 向Redis设置值(优化后:复用连接池,使用fastjson2序列化)
* @param key 键
* @param value 值
* @param expire 过期时间(秒)
*/
public void set(String key, Object value, long expire) {
try {
// 使用fastjson2序列化,避免重复创建ObjectMapper
String jsonValue = JSONObject.toJSONString(value);
// 使用StringRedisTemplate(底层使用连接池)
stringRedisTemplate.opsForValue().set(key, jsonValue, expire, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("Redis设置值失败,key:{}", key, e);
}
}
}
ORDER_CACHE为Redis缓存(自带过期机制);Cache(支持过期和容量限制)。优化后的订单缓存逻辑:
// 替换为Guava Cache,设置过期时间和最大容量
privatestaticfinal LoadingCache<String, JSONObject> ORDER_CACHE = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES) // 30分钟过期
.maximumSize(10000) // 最大缓存1万个订单
.build(new CacheLoader<>() {
@Override
public JSONObject load(String orderId) {
// 缓存未命中时,从数据库查询
return JSONObject.from(getOrderDetail(orderId));
}
});
/**
* 创建订单时缓存订单信息(优化后)
*/
public OrderDTO createOrder(OrderCreateDTO createDTO) {
OrderDTO orderDTO = orderMapper.insertOrder(createDTO);
// 缓存订单信息(自动过期)
JSONObject orderJson = JSONObject.from(getOrderDetail(orderDTO.getId()));
ORDER_CACHE.put(orderDTO.getId(), orderJson);
return orderDTO;
}
功能 | JConsole | JVisualVM |
|---|---|---|
基础监控(内存、线程、类) | 支持 | 支持(界面更友好) |
CPU采样/内存采样 | 不支持 | 支持(核心功能) |
GC可视化 | 不支持 | 支持(需安装Visual GC插件) |
插件扩展 | 不支持 | 支持(丰富的插件生态) |
远程监控 | 支持(JMX) | 支持(JMX、Attach等多种方式) |
上手难度 | 低 | 中(高级功能需学习) |
性能影响 | 极小 | 采样时极小,导出快照时较大 |