卡住时需提前配置JVM参数并即时抓取线程快照:启动时必加-XX:+PrintGCDetails、-XX:+UnlockDiagnosticVMOptions等,卡住后立即执行jstack -l连续三次,容器中需配置SYS_PTRACE权限及正确PID命名空间。

程序卡住但没崩溃、没报错、CPU也不高,这种时候最怕的就是一杀进程,线索全丢。OpenJDK 本身不提供“自动保存现场”功能,得靠你提前布防 + 即时响应,核心是:让 JVM 在卡住时还能被“拍下快照”,且关键信息不被覆盖或丢失。
必须提前加的 JVM 参数
等卡住了再加参数?晚了。这些参数要在启动时就带上,否则 jstack 可能拿不到锁信息,jmap 读不到堆对象,甚至 dump 文件本身都可能被截断:
-
-XX:+PrintGCDetails和-Xlog:gc*:file=gc.log:time(JDK 9+)——GC 行为是卡顿常见诱因,没 GC 日志等于盲人摸象 -
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=jvm.log——捕获 VM 内部事件,比如 safepoint 进入卡住、biased lock 撤销失败等“看不见的阻塞” -
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/——虽然不是 OOM 场景,但有些卡住(如元空间耗尽、类加载死锁)会先触发隐式 OOM,有 dump 就多一条路 - 别漏掉
-Djdk.attach.allowAttachSelf=true(尤其容器环境)——否则 jstack/jmap 可能连不上自己,报Unable to open socket file
jstack -l 是卡住时第一反应
看到接口超时、线程池积压、Spring Boot actuator /health 返回 slow,别等日志刷出错再动——立刻执行:
jstack -l <pid> > threaddump_$(date +%s).txt
-l 很关键:它会额外打印 java.util.concurrent 锁(如 ReentrantLock、StampedLock)的持有者和等待者,没有它,死锁可能藏在 parking to wait for 这种模糊描述里。注意两点:
- 如果进程已僵死但没退出,
jstack仍大概率成功;若提示Unable to get pid of LinuxThreads manager thread,说明 JVM 已彻底挂住,得靠gcore或提前配置的core dump - 别只抓一次:卡住是动态过程,连续抓 3 份间隔 2 秒的 dump,对比线程状态变化(比如某个线程一直在
WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject),才能确认是不是真死锁
容器环境要绕开两个坑
K8s 里 jstack 常失败,不是工具不行,是环境限制:
-
权限问题:默认 Pod 安全上下文是
runAsNonRoot,但jstack需要 attach 到目标进程,得加securityContext.capabilities.add: [SYS_PTRACE],否则报Operation not permitted -
PID namespace 隔离:容器内
ps看到的 PID 是 1, 2, 3…,但宿主机上对应的是大数字。用nsenter -t <host_pid> -p -m -u -i -n jstack -l 1</host_pid>才能正确 attach,或者直接在容器启动时加--pid=host - 更稳妥的做法:把
jstack打包进基础镜像,写个 readiness probe 脚本,一旦探测失败(如 HTTP 响应超时),自动触发 dump 并上传到 S3/NFS
真正难的不是怎么抓 dump,而是抓完之后发现线程全停在 Unsafe.park、锁地址对不上、或者几十个线程在等同一个 java.lang.ref.ReferenceQueue ——这时候得结合 jmap -histo:live 看对象引用链,或者用 jcmd <pid> VM.native_memory summary</pid> 查堆外内存是否耗尽。现场保留只是第一步,线索是否完整,取决于你启动时埋了多少钩子。


















