核心概念:Java 线程 vs. 操作系统线程
首先要理解,一个 Java 线程通常最终会映射到一个操作系统(Linux)的轻量级进程(LWP - Lightweight Process)上,Java 线程的状态(RUNNABLE, BLOCKED, WAITING 等)与操作系统线程的运行状态不完全对应,这是我们分析时需要注意的。

jstack - 最常用、最直接
jstack (Java Stack Trace) 是 JDK 自带的工具,专门用于生成 Java 虚拟机中各个线程的堆栈跟踪信息,这是排查线程问题(如死锁、长时间运行、CPU 100%)的首选方法。
找到 Java 进程 ID (PID)
你需要查看正在运行的 Java 进程。
# 使用 ps 命令 ps -ef | grep java # 或者使用 pgrep 命令(更简洁) pgrep -f java
输出示例:
# ps -ef | grep java
myuser 12345 1234 99 10:30 pts/0 00:15:30 java -jar my-application.jar
myuser 12350 1231 0 10:31 pts/1 00:00:00 grep --color=auto java
这里的 12345 就是我们要找的 Java 进程的 PID。

使用 jstack 生成线程快照
# 基本用法 jstack <PID> > thread_dump.txt # 示例 jstack 12345 > thread_dump_20251027.txt
分析 jstack 输出
打开生成的 thread_dump.txt 文件,你会看到类似下面的内容:
"main" #1 prio=5 os_prio=0 tid=0x00007f8c1c000000 nid=0x1234 runnable [0x00007f8c2a2d0000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:167)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.example.MyApplication.main(MyApplication.java:25)
"pool-1-thread-1" #10 prio=5 os_prio=0 tid=0x00007f8c1c00a000 nid=0x1235 waiting on condition [0x00007f8c292c9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000d1bf2c50> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.Condition.await(Condition.java:203)
at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
at com.example.TaskWorker.run(TaskWorker.java:30)
at java.util.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
如何解读:
"main": 线程的名称。#1: 线程 ID (JVM 内部 ID)。nid=0x1234: 线程 ID (操作系统 Native ID),nid是 Native ID 的缩写,这个 ID 在top或pidstat中可以看到。java.lang.Thread.State: RUNNABLE: 这是最重要的信息,它告诉你线程当前在 JVM 中的状态。RUNNABLE: 线程正在执行或准备执行。WAITING (parking): 线程在等待某个条件,Object.wait()或LockSupport.park()。TIMED_WAITING: 线程在等待某个条件,但设置了超时,Thread.sleep()或Lock.tryLock(long, TimeUnit)。BLOCKED (on object monitor): 线程在等待获取一个锁,被阻塞了。TERMINATED: 线程已执行完毕。
- 后续的堆栈跟踪:显示了线程在执行到当前状态时,所有的方法调用路径,这对于定位问题代码至关重要。
jconsole / VisualVM - 图形化界面分析
如果你更喜欢图形化界面,可以使用 JDK 自带的 jconsole 或更强大的 VisualVM。
启动工具
# jconsole jconsole # VisualVM (推荐,功能更全) jvisualvm
启动后,在左侧的进程列表中选择你要监控的 Java 应用。

在 VisualVM 中查看线程
- 连接到进程:双击进程。
- 切换到 "Threads" 标签页:
- 线程概览:你可以看到所有线程的列表,包括它们的名称、状态(
RUNNABLE,WAITING等)和 CPU 时间。 - 线程 Dump:点击 "Thread Dump" 按钮,可以生成当前的线程快照,内容与
jstack类似,但格式化得更好。 - 死锁检测:如果发生死锁,VisualVM 会自动检测并高亮显示死锁的线程和它们等待的锁。
- CPU 分析:可以查看哪个线程占用了最多的 CPU 时间,这对于定位 CPU 高耗用问题非常有用。
- 线程概览:你可以看到所有线程的列表,包括它们的名称、状态(
top / htop - 结合 OS 线程 ID
这种方法可以让你从操作系统层面快速定位到哪个 Java 线程消耗了最多的 CPU。
使用 top 查看高 CPU 占用进程
top -p <PID>
top -p 12345,你会看到类似这样的输出:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 myuser 20 0 1.5g 800m 100m R 99.9 5.0 15:30.23 java
%CPU 列显示了进程的 CPU 使用率,如果很高,说明 Java 进程内部有线程在疯狂计算。
转换为 OS 线程 ID 并排序
要看到是哪个线程在占用 CPU,需要使用 -H 参数(显示线程)和 -p 参数(指定进程)。
top -H -p <PID>
输出示例:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12356 myuser 20 0 1.5g 800m 100m R 99.9 5.0 15:30.23 java <-- 这是我们要找的线程
12345 myuser 20 0 1.5g 800m 100m S 0.1 5.0 15:30.23 java
这里的 12356 就是操作系统层面的线程 ID (LWP ID)。
关联 jstack 输出
你有了 OS 线程 ID 12356,回到 jstack 的输出文件中,查找 nid=0x12356 (注意,top 的 PID 是十进制,jstack 的 nid 是十六进制)。
# 使用 grep 搜索 grep "nid=0x12356" thread_dump.txt
或者直接搜索线程名(如果线程名有特征),这样你就能精确定位到是哪段 Java 代码导致了高 CPU。
/proc 文件系统 - Linux 原生方法
Linux 的 /proc 文件系统提供了一个虚拟的文件系统来访问内核数据,你可以直接读取它来获取线程信息。
查看进程下的所有线程
一个进程的所有线程都列在 /proc/<PID>/task/ 目录下。
# 列出 PID 12345 的所有线程 ID ls /proc/12345/task/
这会返回所有线程的 OS ID 列表。
查看单个线程的栈
对于 /proc/<PID>/task/<TID>/ 目录,你可以查看其 stack 文件,这通常和 jstack 的输出类似。
# 查看 TID 12356 的栈 cat /proc/12345/task/12356/stack
这个输出通常比 jstack 简洁,直接显示了内核态的调用栈,对于分析某些特定问题(如系统调用阻塞)很有帮助。
总结与最佳实践
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
jstack |
最常用,信息全面,输出可保存,易于脚本化 | 需要手动操作,是静态快照 | 日常线程问题排查,如死锁、长时间等待、CPU 高占用后的代码定位 |
jconsole/VisualVM |
图形化界面,直观,可实时监控,内置死锁检测 | 需要图形界面,可能对性能有轻微影响 | 实时监控,快速定位死锁,初步分析 CPU/内存 |
top/htop |
快速找到高 CPU 占用的 OS 线程,无需安装额外工具 | 信息有限,需要手动关联到 Java 代码 | 快速响应 CPU 100% 的紧急情况,先找到“罪魁祸首”线程 |
/proc |
Linux 原生,无需任何工具,信息来自内核 | 输出格式不易读,需要了解 Linux 内核知识 | 深入系统层面分析,或在无法安装 JDK 工具的受限环境中使用 |
推荐的工作流程:
- 发现问题:通过
top或htop发现 Java 进程 CPU 占用异常高。 - 定位线程:使用
top -H -p <PID>找到具体是哪个 OS 线程(PID)在消耗 CPU。 - 生成快照:使用
jstack <PID>生成一份完整的线程快照。 - 分析关联:将
top中找到的 OS 线程 ID(转换为十六进制)在jstack输出中搜索,定位到具体的 Java 代码和方法。 - 持续监控:如果问题复杂,使用
VisualVM进行持续监控和更深入的分析。
