先记住这个答案
先用 top -H -p <PID> 按 CPU 排序,从输出中记下高占用线程的 TID;或运行 pidstat -t -p <PID> 1 观察 %CPU。然后把 TID 转成十六进制(如 printf "%x\n" TID),在 jstack 输出中查找对应 nid,即可定位到具体业务代码;对非 Java 进程可用 /proc/<PID>/task/<TID>/stack 查看内核栈。若线程名有语义可直接在 ps -L 或 /proc 里查看。
- 用 top -H 或 pidstat -t 查线程级CPU
- TID需转十六进制与线程栈nid对应
- 结合jstack或/proc查看具体执行栈
线程级CPU查看与TID识别机制
进程是资源分配单位,线程才是调度单位,因此进程CPU高不说明哪段代码在跑。top -H 让top按线程列表展示,每行显示线程的TID和CPU占用,进程内的线程都带相同PID但不同TID。pidstat -t 依赖/proc系统中每个线程的状态采样,能输出更精确的时间片占用,例如pidstat -t -p 1234 1每秒打印一次该进程下所有线程的CPU。注意普通用户可能需要root才能看到全量信息。
得到高CPU线程的TID后,若要定位到源码级别,需弄清该线程在用户态的执行位置。对于Java,TID在JVM线程栈中表现为nid=0x...的十六进制值,所以printf "%x" TID后去线程dump里搜即可。对于C/C++,可先使用gdb -p <PID> attach整个进程,再用info threads找到对应TID并切换,或直接使用perf top -t <TID>获取调用栈。/proc/PID/task/TID/下也有进程状态字段,但不含栈回溯。
生产环境Java进程CPU居高不下的定位流程
假设有一个Java订单服务运行于PID 12345,在高峰时CPU超80%,需要定位具体线程。首先执行top -b -n 1 -H -p 12345,记录输出中%CPU最高的TID,假设是21983。然后printf "%x" 21983得到55df,再用jstack 12345抓取线程堆栈,搜索nid=0x55df的线程,看到其堆栈正卡在BigDecimal.divide的循环里,定位到业务代码某处因数据异常导致无限迭代。
为什么先整体后线程?因为直接jstack可能错过瞬时占用,top -H能抓拍当前热点。使用-b批次输出适合脚本,但要注意非交互模式只跑一次可能不准,至少采样几次取稳定高值。这个流程避免盲目dump所有线程,节省排查时间,也避免对生产造成额外性能压力。
适用于CPU高但线程状态非R的边界条件
当线程因IO等待或锁阻塞时,CPU占用低,top -H不会显示高亮,这时要查看线程状态(如/proc/<TID>/status中的State字段)及磁盘I/O等待情况,例如使用pidstat -d。若线程处于D状态(不可中断睡眠),top中CPU可能为0但系统load很高,需检查内核栈或IO。top -H只反映瞬时采样,对于极短促的高CPU波动可能捕捉不到,应当结合pidstat -t 间隔持续采样。
若进程是多线程的,而高CPU线程可能是内核线程(如ksoftirqd)或非业务线程,转十六进制在业务堆栈里搜不到。此时应先用ps -L -p PID查看线程名,或用top -H -p PID配合-o THREAD_NAME查看线程名,从而判断是否为垃圾回收线程或系统线程。若确认是JVM内部线程,还需调整GC参数或检查资源配置。
容易答错的地方
- 用top看PID就足够了
- 只看到进程PID的CPU占用,无法判断是哪段代码占有资源。必须用top -H或pidstat -t列出线程级数据,再结合其它手段定位具体线程的执行栈。
- 认为TID可以直接匹配
- 线程栈中的nid往往是十六进制,比如0xabcd,而TID是十进制。若直接搜十进制TID会找不到对应线程,务必先转成十六进制,注意大小写。
面试官还会怎么问?
定位到高CPU线程后,如何进一步分析用户态函数调用链?
C/C++可用gdb attach整个进程后,用info threads找到目标线程并thread N切换,然后bt,或perf record -t TID采样后用perf report。Java可多次执行jstack抓取堆栈并比对,观察线程栈的变化。内核态可从/proc/<TID>/stack查看,或用perf top -t <TID>。
如果top -H当前看不到高CPU线程,但进程在跑,如何确认线程是否迁移?
线程可能切换执行核心,但top -H的%CPU是累计值,不会因切换而分散。若当前未显示高占用,可结合pidstat持续采样累计各线程CPU时间,或用perf stat -I采样。若线程周期性爆发,需拉长时间窗口。
如何区分是业务代码问题还是JVM内部原因导致的CPU高?
根据线程名判断。GC线程高可能是内存分配压力,编译线程可能是JIT。看线程栈是否栈顶在native或JVM内部。结合jstat -gcutil看GC占比,必要时调整堆参数。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。