每日一题--JVM线程分析与死锁排查

JVM线程分析与死锁排查实战:从现象到原理

问题背景

某电商系统的订单处理服务突然出现响应缓慢 ,部分请求完全无响应。运维人员发现:

  • CPU使用率正常(30%左右)
  • 线程数激增
  • 接口调用超时

一、问题分析思路

1.1 现象梳理

现象 可能原因
线程数激增 大量请求堆积,线程池不断创建新线程
CPU使用率正常 线程处于等待状态,不占用CPU
响应缓慢/无响应 请求被阻塞,无法正常处理

1.2 推理过程

核心思考路径:

  1. 线程数激增 + CPU正常 → 线程大概率在等待(IO等待、锁等待)
  2. 等待的线程占用内存 → 每个线程都有自己的栈内存
  3. 内存持续被占用 → 触发Full GC
  4. Full GC无法回收 → 所有线程都是强引用,仍在运行
  5. 内存不足 → OOM
  6. 应用捕获异常 → 进程未退出,但服务已不可用

关键洞察:

线程在等待状态下,占用内存但不占用CPU,这就解释了为什么CPU正常但服务不可用。

二、模拟实验:复现问题场景

2.1 代码设计

为了验证上述推理,我设计了一个死锁模拟程序:

java 复制代码
// 模拟死锁导致的线程堆积
public class ThreadOOM {
    private ThreadPoolExecutor threadPool;
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public ThreadOOM() {
        // 创建无限增长的线程池
        threadPool = new ThreadPoolExecutor(
            10,              // 核心线程数
            Integer.MAX_VALUE, // 最大线程数(无限制)
            60, TimeUnit.SECONDS,
            new SynchronousQueue<>() // 直接提交,不缓存
        );
    }
    
    public void createDeadlockTask() {
        // 提交死锁任务
        threadPool.execute(() -> {
            synchronized(lock1) {
                try { Thread.sleep(100); } catch(Exception e) {}
                synchronized(lock2) {
                    System.out.println("线程" + Thread.currentThread().getId() + "执行完成");
                }
            }
        });
        
        threadPool.execute(() -> {
            synchronized(lock2) {
                try { Thread.sleep(100); } catch(Exception e) {}
                synchronized(lock1) {
                    System.out.println("线程" + Thread.currentThread().getId() + "执行完成");
                }
            }
        });
    }
    
    public static void main(String[] args) {
        ThreadOOM demo = new ThreadOOM();
        // 不断提交死锁任务,造成线程堆积
        while(true) {
            demo.createDeadlockTask();
            try { Thread.sleep(10); } catch(Exception e) {}
        }
    }
}

2.2 预期效果

  • 死锁导致线程无法释放
  • 线程池不断创建新线程
  • 内存持续增长
  • CPU保持低位(线程都在等待锁)
  • 最终OOM(如果未捕获异常,进程退出;如果捕获,进程存活但服务不可用)

三、JDK工具实战排查

3.1 第一步:定位Java进程

bash 复制代码
# 查看所有Java进程
jps -l

# 输出示例
12345 com.example.ThreadOOM
23456 org.apache.catalina.startup.Bootstrap

3.2 第二步:查看系统资源

bash 复制代码
# 查看进程线程数
top -H -p 12345

# 查看进程内存占用
ps -aux | grep 12345

3.3 第三步:分析线程状态(jstack)

bash 复制代码
# 打印线程栈信息
jstack -l 12345 > thread_dump.txt

jstack输出关键信息解读:

复制代码
"pool-1-thread-123" #123 prio=5 os_prio=0 tid=0x00007f8a3400 nid=0x2a3c waiting for monitor entry [0x00007f8a2fbfe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.ThreadOOM.lambda$createDeadlockTask$1(ThreadOOM.java:28)
    - waiting to lock <0x000000076b5c8d58> (a java.lang.Object)
    - locked <0x000000076b5c8d68> (a java.lang.Object)

"pool-1-thread-124" #124 prio=5 os_prio=0 tid=0x00007f8a3400 nid=0x2a3d waiting for monitor entry [0x00007f8a2fafe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.ThreadOOM.lambda$createDeadlockTask$0(ThreadOOM.java:20)
    - waiting to lock <0x000000076b5c8d68> (a java.lang.Object)
    - locked <0x000000076b5c8d58> (a java.lang.Object)

Found one Java-level deadlock:
=============================
"pool-1-thread-123":
  waiting to lock monitor 0x00007f8a4400 (object 0x000000076b5c8d58, a java.lang.Object),
  which is held by "pool-1-thread-124"
"pool-1-thread-124":
  waiting to lock monitor 0x00007f8a4400 (object 0x000000076b5c8d68, a java.lang.Object),
  which is held by "pool-1-thread-123"

Java stack information for the threads listed above:
===================================================
"pool-1-thread-123":
    at com.example.ThreadOOM.lambda$createDeadlockTask$1(ThreadOOM.java:28)
    - waiting to lock <0x000000076b5c8d58> (a java.lang.Object)
    - locked <0x000000076b5c8d68> (a java.lang.Object)
"pool-1-thread-124":
    at com.example.ThreadOOM.lambda$createDeadlockTask$0(ThreadOOM.java:20)
    - waiting to lock <0x000000076b5c8d68> (a java.lang.Object)
    - locked <0x000000076b5c8d58> (a java.lang.Object)

关键发现:

jstack最后会自动检测并报告死锁!这是排查死锁最直接的方式。

3.4 第四步:监控GC情况(jstat)

bash 复制代码
# 每1秒打印一次GC情况,共打印10次
jstat -gcutil 12345 1000 10

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT   
0.00   0.00  85.23  92.45  95.23  87.34  1245   23.456   78   156.234   0     0.000  179.690
0.00   0.00  86.12  93.78  95.23  87.34  1245   23.456   79   158.456   0     0.000  181.912
0.00   0.00  87.45  95.12  95.23  87.34  1245   23.456   80   160.789   0     0.000  184.245

指标解读:

  • E(Eden区):使用率持续高位
  • O(老年代):不断增长,接近100%
  • FGC(Full GC次数):持续增加
  • FGCT(Full GC耗时):累计时间不断增长

结论: 内存压力巨大,频繁Full GC,但无法回收(线程强引用)。

3.5 第五步:分析堆内存(jmap)

bash 复制代码
# 生成堆转储快照
jmap -dump:live,format=b,file=heap.bin 12345

# 查看堆内存概要
jmap -heap 12345

Attaching to process ID 12345, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1431306240 (1365.0MB)
   OldSize                  = 179306496 (171.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 1073741824 (1024.0MB)
   used     = 989855744 (944.0MB)
   free     = 83886080 (80.0MB)
   92.1875% used

PS Old Generation
   capacity = 2147483648 (2048.0MB)
   used     = 2046820352 (1952.0MB)
   free     = 100663296 (96.0MB)
   95.3125% used

四、JDK工具速查表

工具名 分类 命令示例 核心作用 适用场景
jps 进程管理 jps -l 列出Java进程 获取PID
jstack 线程分析 jstack -l <pid> 打印线程栈 死锁、线程卡顿
jstat 性能监控 jstat -gcutil <pid> 1000 GC监控 内存压力分析
jmap 内存分析 jmap -heap <pid> 堆内存信息 内存泄漏
jinfo 配置查看 jinfo <pid> JVM参数 确认启动参数

jstack输出状态解读

线程状态 含义 排查重点
RUNNABLE 正在执行 检查是否CPU高占用
BLOCKED 等待锁 查看锁持有者,分析死锁
WAITING 无限期等待 检查是否永远等不到通知
TIMED_WAITING 限期等待 通常正常,除非大量堆积
TERMINATED 已结束 正常状态

五、问题根因总结

5.1 问题成因

  1. 代码层面:存在死锁,线程互相等待对方释放锁
  2. 线程池配置:最大线程数无限制,导致线程无限创建
  3. 内存层面:每个线程占用约1MB栈内存,线程数激增耗尽内存
  4. 异常处理:捕获OOM异常,进程未退出,但服务已不可用

5.2 排查路径复盘

复制代码
现象观察
    ↓
CPU正常 + 线程数激增 → 推断线程在等待
    ↓
jstack确认死锁
    ↓
jstat确认频繁FGC
    ↓
jmap确认内存耗尽
    ↓
定位代码问题

5.3 解决方案

  1. 修复死锁 :统一锁获取顺序,或使用tryLock带超时
  2. 合理配置线程池
    • 设置合理的最大线程数
    • 使用有界队列
    • 配置拒绝策略
  3. 添加监控告警
    • 线程数监控
    • GC频率监控
    • 死锁检测

六、面试考点提炼

常见问题

  1. Q:CPU正常但服务无响应,可能是什么问题?

    A:线程处于等待状态(锁等待、IO等待),不占用CPU但占用内存。

  2. Q:jstack如何检测死锁?

    A:JVM会定期检查线程间的锁依赖关系,如果形成循环等待链,jstack会打印死锁信息。

  3. Q:线程数激增一定会导致CPU升高吗?

    A:不一定。如果线程都在等待(BLOCKED/WAITING),CPU使用率可能很低。

  4. Q:Full GC频繁但内存无法释放,可能原因?

    A:所有对象都是强引用且仍在使用(如存活线程的栈引用),GC无法回收。

七、总结

这次问题排查让我深刻理解了线程状态与资源占用的关系

  • 运行中的线程 → 占用CPU
  • 等待中的线程 → 占用内存但不占用CPU
  • 死锁的线程 → 永久等待,内存被持续占用

JDK工具是Java开发者的"瑞士军刀",掌握它们能让我们在遇到问题时快速定位根因,而不是靠猜测重启。

最后的小技巧:当服务出问题时,先别急着重启,用jstack看看线程状态,往往能发现意想不到的线索!


这篇题解是我在实际排查过程中的思考和总结,希望能帮助到遇到类似问题的你。如果有更好的思路或建议,欢迎交流讨论!

相关推荐
xuxie995 小时前
NEXT 1 进程2
java·开发语言·jvm
weisian1518 小时前
JVM--19-面试题5:说说JVM的类加载机制和双亲委派模型
jvm·双亲委派模型·jvm类加载机制
亓才孓9 小时前
【反射机制】
java·javascript·jvm
Volunteer Technology9 小时前
JVM之性能优化
jvm·python·性能优化
Andy Dennis10 小时前
Java语法注意事项
java·开发语言·jvm
坚持的小马11 小时前
JVM相关笔记-jps
jvm·笔记
昱宸星光11 小时前
Xnio源码分析
java·jvm·spring
@insist12311 小时前
软考-数据库系统工程师-计算机存储层次结构与性能优化核心知识点
大数据·jvm·数据库
乂爻yiyao12 小时前
Minecraft 服务端 JVM 调优指南(低资源 / 非专用服务器专用)
运维·服务器·jvm