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

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)

🔈PS: 最近多了一个新的写作方向,就是会把出版社寄来的【java深度调试技术】按照书的骨架提取其中干货进行丰盈写成博客,大家可以关注我的新专栏

📖【java 深度调试】第一章主要介绍了,如何打印出jvm的堆栈信息,以及如何获取jvm中的关键信息。

也讲到了本地线程和java线程的关系,以及如何进行映射。

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

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

java 堆栈分析

打印jvm堆栈

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

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

js 复制代码
-- 执行命令
ps -ef | grep test-java.jar
-- 输出--- 2982718 就是我们的PID 了
 root     2982718       1  1 10月14 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容器中运行的,所以执行这些命令也有点区别

shell 复制代码
-- 进入容器
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

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

js 复制代码
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

认识堆栈

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

vbnet 复制代码
"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 线程的所有行为最终都依赖本地线程执行。

js 复制代码
"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线程了。

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

死锁分析

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

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

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

js 复制代码
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

js 复制代码
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的使用情况

js 复制代码
-- 执行 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就行了
js 复制代码
-- 转了进制
printf "0x%x\n" 808923
0xc57db

搜索结果:

js 复制代码
"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 深度调式技术] 第一章堆栈分析内容为主线,也在其内容骨架上做了一些补充吧。希望本片文章能对你起到一定的作用。

相关推荐
lastHertz4 小时前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥4 小时前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜4 小时前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始5 小时前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴5 小时前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年5 小时前
用Go实现类似WinGet风格彩色进度条
后端
蓝色汪洋5 小时前
string字符集
java·开发语言
卿言卿语5 小时前
CC1-二叉树的最小深度
java·数据结构·算法·leetcode·职场和发展
淳朴小学生5 小时前
静态代理和动态代理
后端