首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JVM 问题排查教程:步骤 + 案例

JVM 问题排查教程:步骤 + 案例

作者头像
SmileNicky
发布2026-06-16 08:19:42
发布2026-06-16 08:19:42
240
举报
文章被收录于专栏:Nicky's blogNicky's blog

JVM 问题排查教程:步骤 + 案例

JVM问题是Java线上故障的核心重灾区,90%的性能问题最终都会指向JVM。本文提供标准化排查框架+几个可复现的实战案例,所有命令可直接复制执行,从基础监控到深度分析全覆盖,新手也能一步步定位问题。


一、前置准备:故障发生前必须做的2件事

1. 配置JVM诊断参数(自动留存证据)

在应用启动脚本中添加以下参数,故障发生时自动生成所有诊断文件,避免错过最佳排查时机:

代码语言:javascript
复制
# 1. OOM自动生成堆转储(最重要)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump-$(date +%Y%m%d-%H%M%S).hprof

# 2. 输出详细GC日志(分析GC问题必备)
-Xloggc:/var/log/gc-$(date +%Y%m%d-%H%M%S).log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintGCApplicationStoppedTime

# 3. 发生OOM时执行告警脚本
-XX:OnOutOfMemoryError="sh /opt/alert.sh %p"

# 4. 开启JFR飞行记录(JDK11+推荐,低开销深度监控)
-XX:+FlightRecorder
-XX:StartFlightRecording=duration=1h,filename=/var/log/recording.jfr
2. 必备工具清单(按优先级排序)

工具类型

推荐工具

适用场景

JDK原生(零安装)

jps、jstat、jstack、jmap、jinfo

所有基础排查

在线诊断神器

Arthas(阿里开源)

生产环境无需重启,一键定位

离线分析工具

HeapHero(在线)、MAT

堆转储深度分析

GC日志分析

GCEasy(在线)

GC问题快速诊断


二、通用排查四步法(所有JVM问题通用)

  1. 收集基础信息:先看应用错误日志、系统监控(CPU/内存/磁盘)、JVM进程状态
  2. 定位问题类型:判断是CPU高、内存泄漏、OOM、死锁还是类加载问题
  3. 深入分析根因:用对应工具抓取线程栈、堆转储、GC日志
  4. 修复验证:修改代码/调整参数,重启后监控指标是否恢复

三、5个高频JVM问题实战案例(附可运行代码)

案例:CPU占用100%(最常见)
现象
  • 系统CPU持续100%,负载飙升
  • 应用响应缓慢,接口超时
  • 服务器SSH连接卡顿
模拟代码(可直接运行)
代码语言:javascript
复制
public class Cpu100Demo {
    public static void main(String[] args) {
        // 启动2个死循环线程,打满CPU
        new Thread(() -> {
            while (true) {
                // 空循环,CPU空转
            }
        }, "DeadLoop-Thread-1").start();

        new Thread(() -> {
            while (true) {
                // 复杂计算
                Math.pow(Math.random(), Math.random());
            }
        }, "DeadLoop-Thread-2").start();
    }
}
详细排查步骤

定位高CPU进程

代码语言:javascript
复制
top
# 按大写P按CPU降序排序,找到Java进程PID(比如:12345)

输出示例:

代码语言:javascript
复制
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
12345 root      20   0  4.5g  1.2g  12345 S  198.7 15.2   12:34.56 java

定位进程内高CPU线程

代码语言:javascript
复制
top -Hp 12345
# 按大写P排序,找到CPU最高的线程TID(比如:12346、12347)

输出示例:

代码语言:javascript
复制
PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
12346 root      20   0  4.5g  1.2g  12345 R 99.3 15.2    5:23.45 java
12347 root      20   0  4.5g  1.2g  12345 R 98.7 15.2    5:12.34 java

线程ID转换为十六进制 jstack输出的线程ID是十六进制,需要转换:

代码语言:javascript
复制
printf "%x\n" 12346  # 输出:303a
printf "%x\n" 12347  # 输出:303b

抓取线程栈定位代码行

代码语言:javascript
复制
jstack 12345 | grep -A 20 -E "303a|303b"

输出示例:

代码语言:javascript
复制
"DeadLoop-Thread-1" #1 prio=5 os_prio=0 tid=0x00007f8a9c001000 nid=0x303a runnable [0x00007f8aa0000000]
   java.lang.Thread.State: RUNNABLE
        at com.example.Cpu100Demo.lambda$main$0(Cpu100Demo.java:7)
        at com.example.Cpu100Demo$$Lambda$1/0x0000000800000000.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"DeadLoop-Thread-2" #2 prio=5 os_prio=0 tid=0x00007f8a9c002000 nid=0x303b runnable [0x00007f8a9f000000]
   java.lang.Thread.State: RUNNABLE
        at com.example.Cpu100Demo.lambda$main$1(Cpu100Demo.java:13)
        at com.example.Cpu100Demo$$Lambda$2/0x0000000800000000.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

直接定位到Cpu100Demo.java第7行和第13行的死循环。

解决方案
  • 修复死循环:添加退出条件
  • 复杂计算分批处理,使用异步线程
  • 避免在循环中执行耗时操作

案例:频繁Full GC(内存泄漏)
现象
  • 应用响应越来越慢,接口超时
  • CPU占用高(GC线程占比超过50%)
  • 系统监控显示堆内存持续上涨
模拟代码
代码语言:javascript
复制
import java.util.HashMap;
import java.util.Map;

public class FullGCDemo {
    // 静态集合导致内存泄漏
    private static final Map<String, byte[]> leakCache = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // JVM参数:-Xms200m -Xmx200m -Xloggc:gc.log
        for (int i = 0; i < 1000000; i++) {
            // 每次添加1KB的字节数组,从不清理
            leakCache.put("key" + i, new byte[1024]);
            Thread.sleep(1);
        }
    }
}
详细排查步骤

监控GC实时状态(核心命令)

代码语言:javascript
复制
# 每隔1秒输出一次GC统计,持续输出
jstat -gcutil 12345 1000

输出示例(异常状态):

代码语言:javascript
复制
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  99.99  99.98  99.99  85.23  82.15   1234   12.345   567   89.012  101.357
0.00  99.99  99.97  99.99  85.23  82.15   1235   12.456   568   89.123  101.579
0.00  99.99  99.99  99.99  85.23  82.15   1236   12.567   569   89.234  101.801

异常指标:FGC(Full GC次数)每秒上涨,FGCT(Full GC总耗时)持续增加,老年代使用率O一直保持99%以上。

怎么判断频繁gc:

  • FGC 数字疯狂上涨(每秒 + 1、每 2 秒 + 1)
  • FGCT 快速上涨(GC 总时间飙升)
  • O 列 ≥ 90%(老年代爆满)
  • YGC 每秒涨好几次

查看对象统计信息

代码语言:javascript
复制
# 只统计存活对象,按大小排序,取前20行
jmap -histo:live 12345 | head -20

输出示例:

代码语言:javascript
复制
 num     #instances         #bytes  class name
----------------------------------------------
   1:        987654      1012345678  [B
   2:        987654        31604928  java.util.HashMap$Node
   3:        987654        23703696  java.lang.String

发现byte[]HashMap$Node对象数量异常多,各接近100万个。

导出堆转储深度分析

代码语言:javascript
复制
jmap -dump:live,format=b,file=heapdump.hprof 12345

用JVisualVM排查:

在这里插入图片描述
在这里插入图片描述

上传到HeapHero(https://heaphero.io/)分析,会直接显示:

在这里插入图片描述
在这里插入图片描述
解决方案

修复静态集合泄漏:使用完对象后调用remove()

改用带过期时间的缓存:Guava Cache或Caffeine

定期清理缓存:设置最大容量和过期时间

代码语言:javascript
复制
// 正确示例:使用Guava Cache
private static final Cache<String, byte[]> safeCache = CacheBuilder.newBuilder()
        .maximumSize(10000) // 最大容量
        .expireAfterWrite(30, TimeUnit.MINUTES) // 30分钟过期
        .build();

案例:堆内存溢出(OOM)
现象
  • 应用崩溃,日志报错:java.lang.OutOfMemoryError: Java heap space
  • 堆转储文件大小接近-Xmx设置的值
模拟代码
代码语言:javascript
复制
import java.util.ArrayList;
import java.util.List;

public class HeapOOMDemo {
    private static final List<byte[]> list = new ArrayList<>();

    public static void main(String[] args) {
        // JVM参数:-Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError
        while (true) {
            // 每次添加10MB的字节数组
            list.add(new byte[10 * 1024 * 1024]);
            System.out.println("已添加:" + list.size() + "个对象");
        }
    }
}
详细排查步骤

确认OOM类型 查看应用错误日志:

代码语言:javascript
复制
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at com.example.HeapOOMDemo.main(HeapOOMDemo.java:9)
Dumping heap to heapdump-20260526-163000.hprof ...
Heap dump file created [104857600 bytes in 0.123 secs]

用JVisualVM分析堆转储

  • 打开JVisualVM打开hprof文件
  • 查看报告:
在这里插入图片描述
在这里插入图片描述

也可以用https://heaphero.io/平台直接分析看看:

在这里插入图片描述
在这里插入图片描述

根因:OOM类的静态list集合持有了大量字节数组。

解决方案
  • 修复内存泄漏:及时释放不再使用的对象
  • 调大堆内存:-Xms4g -Xmx4g(根据业务需求)
  • 避免一次性加载大量数据:分页查询、分批处理

案例:元空间溢出
现象
  • 应用启动失败或运行中崩溃,日志报错:java.lang.OutOfMemoryError: Metaspace
  • 常见于使用大量动态代理的项目
模拟代码
代码语言:javascript
复制
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceOOMDemo {
    public static void main(String[] args) {
        // JVM参数:-XX:MaxMetaspaceSize=50m
        while (true) {
            // 动态生成大量类,不缓存
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false); // 关键:关闭缓存
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> 
                proxy.invokeSuper(obj, args1));
            enhancer.create();
        }
    }
}
详细排查步骤

查看元空间使用情况

代码语言:javascript
复制
jstat -gcmetacapacity 12345

输出示例:

代码语言:javascript
复制
MCMN       MCMX        MC       CCSMN      CCSMX       CCSC     YGC   FGC    FGCT     GCT
   0.0  51200.0    50987.0       0.0    8192.0     7890.0   123    45    12.345   23.456

MC(当前元空间大小)已经接近MCMX(最大元空间大小)。

查看类加载统计

代码语言:javascript
复制
jmap -clstats 12345

输出会显示已加载类数量异常多(超过10万个)。

解决方案
  • 调大元空间大小:-XX:MaxMetaspaceSize=256m
  • 开启CGLIB缓存:enhancer.setUseCache(true)
  • 避免在循环中动态生成类
  • 升级框架版本,修复类加载器泄漏

四、核心工具使用速查表

JDK原生工具

工具

常用命令

作用

jps

jps -l

列出所有Java进程PID和主类名

jstat

jstat -gcutil <PID> 1000

实时监控GC情况

jstack

jstack <PID> > threaddump.txt

抓取线程栈

jmap

jmap -histo:live <PID>

查看对象统计

jmap

jmap -dump:live,format=b,file=heap.hprof <PID>

导出堆转储

jinfo

jinfo -flags <PID>

查看JVM启动参数

Arthas常用命令

命令

作用

dashboard

实时查看CPU、内存、线程、GC

thread -n 3

查看最耗CPU的3个线程

thread -b

查看阻塞其他线程的死锁线程

heapdump /tmp/heap.hprof

导出堆转储

jmap -histo

查看对象统计


五、最佳实践与预防措施

  1. 提前配置JVM诊断参数:开启OOM自动dump和GC日志
  2. 建立监控告警体系
    • CPU使用率:阈值80%
    • 堆内存使用率:阈值80%
    • Full GC频率:阈值每分钟1次
    • 线程数:阈值1000
  3. 代码规范
    • 避免滥用静态集合
    • 使用try-with-resources自动关闭资源
    • 统一锁获取顺序
    • 合理使用线程池
  4. 压测验证:上线前进行压力测试,模拟真实业务量
  5. 定期巡检:每月分析一次GC日志和堆转储,排查潜在问题

六、快速排查清单

代码语言:javascript
复制
查看占用cpu最高的进程
top
查看进程的线程
top -Hp pid
打印线程id对应的16进制
printf "%x\n" tid
查看堆栈信息,tid是16进制的
jstack pid | grep tid -A 30
检查gc是否频繁
jstat -gcutil pid 1000
导出堆栈文件
jmap dump:format=b,file=heap.hprof PID 
使用MAT、jvisualvm、heaphero网站等查看堆栈文件
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JVM 问题排查教程:步骤 + 案例
    • 一、前置准备:故障发生前必须做的2件事
      • 1. 配置JVM诊断参数(自动留存证据)
      • 2. 必备工具清单(按优先级排序)
    • 二、通用排查四步法(所有JVM问题通用)
    • 三、5个高频JVM问题实战案例(附可运行代码)
      • 案例:CPU占用100%(最常见)
      • 案例:频繁Full GC(内存泄漏)
      • 案例:堆内存溢出(OOM)
      • 案例:元空间溢出
    • 四、核心工具使用速查表
      • JDK原生工具
      • Arthas常用命令
    • 五、最佳实践与预防措施
    • 六、快速排查清单
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档