前言
我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)
🔈PS: 最近多了一个新的写作方向,就是会把出版社寄来的【java深度调试技术】按照书的骨架提取其中干货进行丰盈写成博客,大家可以关注我的新专栏
📖【java 深度调试】第一章主要介绍了,如何打印出jvm的堆栈信息,以及如何获取jvm中的关键信息。
也讲到了本地线程和java线程的关系,以及如何进行映射。
讲了这些基础知识之后,然后就列了一些通过分析堆栈信息解决问题的案例:比如死锁分析、死循环导致的CPU使用率过高、cpu标高、资源不足性能下降的分析、系统挂死分析,多个锁导致的锁链分析。
Java堆栈信息(Stack Trace)记录了程序运行中某个时刻的线程执行上下文,包括方法的调用链和代码位置。主要分为两类:线程堆栈(线程转储)、异常堆栈 今天主要就是介绍的 线程堆栈(线程转储)了
java 堆栈分析
打印jvm堆栈
书中分析了 window
和 linux
下打印堆栈的方式,这里的话我就直接介绍linux
环境了。
首先我们要确认java的进程号PID:
-- 执行命令
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
容器中运行的,所以执行这些命令也有点区别
-- 进入容器
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)
-
线程名称:
MyScheduler_QuartzSchedulerThread
这是 Quartz 调度器的核心线程(QuartzSchedulerThread
),名称前缀MyScheduler
可能是用户自定义的调度器名称。 -
线程优先级:
prio=5
(默认优先级,范围 1-10,5 为中等)os_prio=0
表示操作系统层面的优先级(不同系统含义可能不同,此处为默认值)。 -
线程 ID 相关:
tid=0x00007f30596c7800
:JVM 内部的线程唯一标识。nid=0x2d84cf
:对应操作系统的线程 ID(可通过ps -Lp <进程ID>
等命令关联系统线程)。
-
线程状态:
TIMED_WAITING (on object monitor)
TIMED_WAITING
:线程处于定时等待状态,即它在等待一个有限的时间后会自动唤醒(区别于无限等待的WAITING
)。(on object monitor)
:表示通过synchronized
获取对象锁后,调用wait(long timeout)
进入等待状态(持有锁时才能调用wait
)。
-
等待位置:
- 原生方法:
java.lang.Object.wait(Native Method)
(线程等待的底层实现)。 - 业务代码:
org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:427)
说明线程在 Quartz 的QuartzSchedulerThread
类的run
方法第 427 行调用了wait
方法。
- 原生方法:
-
锁定的对象:
- 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
- 转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.................
系统挂死分析
打开堆栈文件,重点关注
- 死锁:搜索
Found one Java-level deadlock
,确认是否有线程相互持有锁并等待(参考之前的死锁分析)。 - 线程池耗尽:查看是否大量线程处于
WAITING
状态(如ThreadPoolExecutor$Worker
等待任务),且activeCount
等于corePoolSize
,说明线程池满,新请求无法处理。 - IO 阻塞:线程状态为
WAITING
且堆栈显示java.net.SocketInputStream.read
、java.io.FileInputStream.read
,说明线程卡在网络请求(如远程服务超时)或磁盘读写(如大文件读取)。
书中还提到 资源不足性能下降的分析,多个锁导致的锁链分析 这些都不再细说了。主要能够掌握分析死锁,以及CPU飙高问题定位的能力就差不多了
总结
以 [java 深度调式技术] 第一章堆栈分析内容为主线,也在其内容骨架上做了一些补充吧。希望本片文章能对你起到一定的作用。