java 深度调试【第一章:堆栈分析】

2 阅读3分钟

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)
🔈PS: 最近多了一个新的写作方向,就是会把出版社寄来的【java深度调试技术】按照书的骨架提取其中干货进行丰盈写成博客,大家可以关注我的新专栏

📖【java 深度调试】第一章主要介绍了,如何打印出jvm的堆栈信息,以及如何获取jvm中的关键信息。
也讲到了本地线程和java线程的关系,以及如何进行映射。

讲了这些基础知识之后,然后就列了一些通过分析堆栈信息解决问题的案例:比如死锁分析、死循环导致的CPU使用率过高、cpu标高、资源不足性能下降的分析、系统挂死分析,多个锁导致的锁链分析。

Java堆栈信息(Stack Trace)记录了程序运行中某个时刻的线程执行上下文,包括方法的调用链和代码位置。主要分为两类:线程堆栈(线程转储)、异常堆栈 今天主要就是介绍的 线程堆栈(线程转储)了

java 堆栈分析

打印jvm堆栈

书中分析了 windowlinux 下打印堆栈的方式,这里的话我就直接介绍linux环境了。

首先我们要确认java的进程号PID:

-- 执行命令
ps -ef | grep test-java.jar
-- 输出--- 2982718 就是我们的PID 了
 root     2982718       1  1 1014 pts/2  00:42:58 /data/java/bin/java -Xms256M -Xmx256M -Dfile.encoding=utf-8 -server -jar /data/online-activity/test-java.jar --spring.profiles.active=dev

确认PID之后,就可以打印堆栈了,打印堆栈命令:

  • kill -3 -pid (输出到java重定向的日志文件中)
  • jstack pid > ./stack.txt

当然现在很多java应用都是在docker容器中运行的,所以执行这些命令也有点区别

-- 进入容器
docker exec -it <container_id> bash
-- 确认java进程PID 
ps aux | grep java 或者 jps

-- 打印堆栈
kill -3 <pid>  或者 jstack <pid> > /tmp/thread_dump.txt

-- 查看(kill -3)打印日志
docker logs <container_id> --tail 100

🔈看一下打印的堆栈信息(部分)

2025-10-16 09:17:26
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode):

"Attach Listener" #7892 daemon prio=9 os_prio=0 tid=0x00007f2ffc002000 nid=0x8669c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"MyScheduler_QuartzSchedulerThread" #69 prio=5 os_prio=0 tid=0x00007f30596c7800 nid=0x2d84cf in Object.wait() [0x00007f2f897de000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:427)
	- locked <0x00000000f43e0468> (a java.lang.Object)

"MyScheduler_Worker-10" #68 prio=5 os_prio=0 tid=0x00007f305a085000 nid=0x2d84ce in Object.wait() [0x00007f2f898df000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:568)
	- locked <0x00000000f43df998> (a java.lang.Object)
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f30581ce000 nid=0x2d834a in Object.wait() [0x00007f3044fe2000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x00000000f0012d80> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0x00007f30581c6800 nid=0x2d8349 runnable 

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f305801f000 nid=0x2d8341 runnable 

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f3058020800 nid=0x2d8342 runnable 

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f3058022800 nid=0x2d8343 runnable 

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f3058024000 nid=0x2d8344 runnable 

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00007f3058026000 nid=0x2d8345 runnable 

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00007f3058027800 nid=0x2d8346 runnable 

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00007f3058029800 nid=0x2d8347 runnable 

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00007f305802b000 nid=0x2d8348 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0x00007f305822d800 nid=0x2d8352 waiting on condition 

JNI global references: 4330

认识堆栈

下面就是其中一个线程的堆栈信息

"MyScheduler_QuartzSchedulerThread" #69 prio=5 os_prio=0 tid=0x00007f30596c7800  
nid=0x2d84cf in Object.wait() [0x00007f2f897de000] java.lang.Thread.State:  
TIMED_WAITING (on object monitor) at java.lang.Object.wait(Native Method) at  
org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:427)   
- locked <0x00000000f43e0468> (a java.lang.Object)
  1. 线程名称MyScheduler_QuartzSchedulerThread这是 Quartz 调度器的核心线程(QuartzSchedulerThread),名称前缀MyScheduler可能是用户自定义的调度器名称。

  2. 线程优先级prio=5(默认优先级,范围 1-10,5 为中等)os_prio=0表示操作系统层面的优先级(不同系统含义可能不同,此处为默认值)。

  3. 线程 ID 相关

    • tid=0x00007f30596c7800:JVM 内部的线程唯一标识。
    • nid=0x2d84cf:对应操作系统的线程 ID(可通过ps -Lp <进程ID>等命令关联系统线程)。
  4. 线程状态TIMED_WAITING (on object monitor)

    • TIMED_WAITING:线程处于定时等待状态,即它在等待一个有限的时间后会自动唤醒(区别于无限等待的WAITING)。
    • (on object monitor):表示通过synchronized获取对象锁后,调用wait(long timeout)进入等待状态(持有锁时才能调用wait)。
  5. 等待位置

    • 原生方法:java.lang.Object.wait(Native Method)(线程等待的底层实现)。
    • 业务代码:org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:427)说明线程在 Quartz 的QuartzSchedulerThread类的run方法第 427 行调用了wait方法。
  6. 锁定的对象- locked <0x00000000f43e0468> (a java.lang.Object)线程在进入等待前,已通过synchronized锁定了一个java.lang.Object实例(地址0x00000000f43e0468),这是 Quartz 内部用于线程同步的监视器对象。

本地线程 和 java 线程的关系

本地线程(操作系统原生线程)和 Java 线程的关系,本质是 “底层实现” 与 “上层抽象” 的绑定,主流 JVM(如 HotSpot)通过 “1:1 映射模型” 将二者强关联,Java 线程的所有行为最终都依赖本地线程执行。

"MyScheduler_QuartzSchedulerThread" #69 prio=5 os_prio=0 tid=0x00007f30596c7800 nid=0x2d84cf in Object.wait() [0x00007f2f897de000] java.lang.Thread.State:
  • tid=0x00007f30596c7800:JVM 内部的线程唯一标识。
  • nid=0x2d84cf:对应操作系统的线程 ID

nid 就是操作系统的线程ID,转成10进制之后,就得到操作系统线程ID:2983119

📖了解了本地线程和java线程,以及能够导出堆栈信息了。
这时候我们就有能力做很多事情了: 比如CPU,通过top 命令(top -H)确认是哪个进程占用CPU比较多,确认进程之后,我们就看这个进程中线程占用CPU的情况(top -H -p <PID>),得到十进制的线程ID,转成16进制在我们堆栈文件搜索就能定位到具体的java线程了。

通过线程堆栈进行问题分析

死锁分析

导出的堆栈会自动识别死锁,所以这种方式就特别简单了

1. 通过jsp -l 或者 ps -ef | grep xx.jar 确认java应用进程PID
2.导出线程堆栈信息
jstack <PID> > thread_dump.txt

得到堆栈文件:搜索 deadlock,死锁在堆栈文件的最后

Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 0x00007f9a880059b8 (object 0x00000000f771aec8, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007f99e0002af8 (object 0x00000000f771aed8, a java.lang.Object),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.method2(FixedDeadlockExample.java:29)
	- waiting to lock <0x00000000f771aec8> (a java.lang.Object)
	- locked <0x00000000f771aed8> (a java.lang.Object)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.lambda$start$1(FixedDeadlockExample.java:44)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample$$Lambda$1097/45320991.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
"Thread-1":
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.method1(FixedDeadlockExample.java:14)
	- waiting to lock <0x00000000f771aed8> (a java.lang.Object)
	- locked <0x00000000f771aec8> (a java.lang.Object)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.lambda$start$0(FixedDeadlockExample.java:40)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample$$Lambda$1096/1733500474.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

🔈如果没有有,可以尝试搜索一下BLOCKED

Thread-2" #74 prio=5 os_prio=0 tid=0x00007f9ad637b000 nid=0xc1872 waiting for monitor entry [0x00007f9a7a845000]
//线程状态阻塞
   java.lang.Thread.State: BLOCKED (on object monitor)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.method2(FixedDeadlockExample.java:29)
        // 等待aec8 对象锁
	- waiting to lock <0x00000000f771aec8> (a java.lang.Object)
        // 持有 aed8对象锁
	- locked <0x00000000f771aed8> (a java.lang.Object)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.lambda$start$1(FixedDeadlockExample.java:44)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample$$Lambda$1097/45320991.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"Thread-1" #73 prio=5 os_prio=0 tid=0x00007f9ad638b800 nid=0xc1871 waiting for monitor entry [0x00007f9a7a946000]
    // 线程阻塞
   java.lang.Thread.State: BLOCKED (on object monitor)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.method1(FixedDeadlockExample.java:14)
        // 等待aed8对象锁
	- waiting to lock <0x00000000f771aed8> (a java.lang.Object)
        // 持有aec8对象锁
	- locked <0x00000000f771aec8> (a java.lang.Object)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample.lambda$start$0(FixedDeadlockExample.java:40)
	at org.jeecg.modules.activity.controller.app.FixedDeadlockExample$$Lambda$1096/1733500474.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

我们也能定位到死锁

CPU飙高分析

这块其实上面的文章已经提到过了第一 确认java 进程ID(jps -l 或者 ps -ef | grep java)
第二步查看进程中线程cpu的使用情况

-- 执行 top -H -p 808921
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 808923 root      20   0 6265968 568864  18696 R  70.4   1.7   3:31.74 java
 808932 root      20   0 6265968 568864  18696 S  10.6   1.7   0:30.10 java
 808924 root      20   0 6265968 568864  18696 S  10.0   1.7   0:27.87 java
 .................
 -- cpu使用率前三的进程,第一个是 808923
  1. 转16进制,并且使用jstack 导出堆栈信息,搜索线程id就行了
-- 转了进制
printf "0x%x\n" 808923
0xc57db

搜索结果:

"main" #1 prio=5 os_prio=0 tid=0x00007f6234009800 nid=0xc57db runnable [0x00007f623bd79000]
   java.lang.Thread.State: RUNNABLE
	at java.lang.Integer.valueOf(Integer.java:832)
	at com.dfkj.activity.controller.app.TestWhileExample.start(TestWhileExample.java:40)
	at com.dfkj.activity.controller.app.OaAppController.test(OaAppCon.................

系统挂死分析

打开堆栈文件,重点关注

  1. 死锁:搜索Found one Java-level deadlock,确认是否有线程相互持有锁并等待(参考之前的死锁分析)。
  2. 线程池耗尽:查看是否大量线程处于WAITING状态(如ThreadPoolExecutor$Worker等待任务),且activeCount等于corePoolSize,说明线程池满,新请求无法处理。
  3. IO 阻塞:线程状态为WAITING且堆栈显示java.net.SocketInputStream.readjava.io.FileInputStream.read,说明线程卡在网络请求(如远程服务超时)或磁盘读写(如大文件读取)。

书中还提到 资源不足性能下降的分析,多个锁导致的锁链分析 这些都不再细说了。主要能够掌握分析死锁,以及CPU飙高问题定位的能力就差不多了

总结

[java 深度调式技术] 第一章堆栈分析内容为主线,也在其内容骨架上做了一些补充吧。希望本片文章能对你起到一定的作用。