《Java 高级编程》附录 D 诊断命令清单

按进程、线程、堆、JFR、字节码、依赖、运行时裁剪和启动参数八层,给出 libraryd 排障会用到的 jps、jcmd、jstack、jmap、jfr、javap、jdeps、jlink、-Xlog、--enable-preview、--enable-native-access 真实命令、观察点与场景,并逐条标注是否在本机执行。

这份清单的口径

本附录的命令都在 macOS + Temurin 25.0.5 上执行过,目标进程是一个 DiagDemo:4 个平台工作线程在 synchronized 块里循环,200 个虚拟线程处于 sleep。除了明确标注「未执行」的命令,命令和输出都是本机原文,路径、PID、耗时随环境变化。

标注约定:

  • [已执行]:本机跑过,输出为原文节选。
  • [未执行]:只给命令和观察点,不伪造输出。

排障顺序仍然是「先分层,再选命令」:

层次先看什么命令
进程进程在不在、启动参数是什么jps、jcmd VM.command_line
线程谁在阻塞、锁被谁持有jcmd Thread.print、jstack
虚拟线程有多少、卡在哪个栈jcmd Thread.dump_to_file -format=json
堆用了多少、谁占大头jcmd GC.heap_info、jmap -histo
采样CPU 热点、睡眠、锁竞争jfr、jcmd JFR.start
字节码方法到底编译成什么javap -c -p
依赖模块边界、JDK 内部 APIjdeps
运行时裁剪后的 JDK 能不能跑jlink

进程与 JVM 概览

jps:先确认进程

jps -lv

本机输出(省略无关进程):

314 DiagDemo
347 jdk.jcmd/sun.tools.jps.Jps -Dapplication.home=/Users/bingrong.yan/.jdks/jdk-25.0.5+8/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd

jps -l 显示主类或 jar 路径,-v 显示 JVM 参数。libraryd 场景:确认容器里跑的是新包还是旧包,以及 -Xmx、--enable-preview 是否真的传给了 JVM。jps 找不到进程时,先查用户是否一致、容器 PID 命名空间是否隔离,不要直接怀疑 JVM。

jcmd:统一入口

jcmd <pid> help

本机输出节选:

The following commands are available:
GC.class_histogram
GC.heap_info
JFR.start
Thread.dump_to_file
Thread.print
VM.command_line
VM.system_properties

jcmd 是首选入口,因为大多数 jstack、jmap、jinfo 的能力都能通过它完成,而且参数更稳定。先 help 看当前 JVM 支持哪些命令,再决定用哪个。

jcmd <pid> VM.command_line
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties | grep -E 'java.version|os.name|user.dir'

命令输出节选(VM.flags 未摘录):

VM Arguments:
java_command: DiagDemo
java_class_path (initial): out
Launcher Type: SUN_STANDARD
java.version=25.0.5

libraryd 场景:线上怀疑「配置没生效」时,先看 VM.command_line 和 VM.system_properties,确认 -D 参数、JAVA_HOME、工作目录,而不是从代码里猜。

线程诊断

jcmd Thread.print 与 jstack

jcmd <pid> Thread.print
jstack <pid>

两者输出格式基本一致。本机抓到的 libraryd-worker:

"libraryd-worker" #25 [28163] daemon prio=5 os_prio=31 cpu=0.81ms elapsed=2.64s
   java.lang.Thread.State: BLOCKED (on object monitor)
	at DiagDemo.lambda$main$1(DiagDemo.java:18)
	- waiting to lock <0x000000035fc184e8> (a java.lang.Object)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)

观察点:

  • BLOCKED (on object monitor) 表示等监视器,waiting to lock <地址> 指向锁对象。
  • WAITING (parking) 常见于 LockSupport.park、ReentrantLock、Future.get。
  • TIMED_WAITING (sleeping) 是 Thread.sleep,不是锁问题。
  • 同一把锁地址被多个线程等待时,找持有者;jcmd Thread.print -l 会打印 java.util.concurrent 锁信息。

libraryd 场景:接口变慢但 CPU 不高,先抓线程栈,看请求线程是否都卡在 Hikari 获取连接、synchronized 或下游 HTTP 读。

Thread.print 默认只列平台线程。本机 200 个虚拟线程没有出现在输出里,只出现了 VirtualThread-unblocker 这类平台线程。抓虚拟线程必须用 Thread.dump_to_file。

Thread.dump_to_file:包含虚拟线程的 JSON

jcmd <pid> Thread.dump_to_file -format=json /tmp/vol3-d/diag/threads.json

本机输出:

Created /tmp/vol3-d/diag/threads.json

文件结构是 threadDump.threadContainers[].threads[],每个线程有 name、state、virtual、stack。本机统计:

containers: 6
container='<root>'                 threads=   8 virtual=   0
container='ForkJoinPool-1/...'     threads=  11 virtual=   0
container='java.util.concurrent.ThreadPerTaskExecutor@...' threads= 200 virtual= 200
container='java.util.concurrent.ThreadPoolExecutor@...'    threads=   4 virtual=   0
total threads: 223

一条虚拟线程的栈顶:

sample virtual name/state/virtual:  TIMED_WAITING True
sample first frames: ['java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:787)',
                      'java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:983)']

libraryd 场景:请求量突增时,平台线程数稳定但延迟上升,用 JSON dump 统计 virtualThreads 数量和状态分布。如果大量虚拟线程停在 parkNanos,是在等超时;停在 socket read,是在等下游;停在 monitor enter,才需要回到锁竞争。默认虚拟线程名为空,建议在 Thread.ofVirtual().name("http-", 0) 里起名字,dump 才好读。

死锁

JVM 能自动检测监视器死锁,并在 Thread.print 的输出末尾给出 Found one Java-level deadlock;直接在 dump 里搜索这段文本即可。ReentrantLock 的死锁不一定被这段文本捕获,需要看 Thread.print -l 里的 waiting to lock 链。libraryd 场景:借阅与归还以相反顺序锁两本书时,统一加锁顺序比事后排查更有效。

堆与类

GC.heap_info

jcmd <pid> GC.heap_info

本机输出:

garbage-first heap   total reserved 8388608K, committed 528384K, used 17371K
 region size 4096K, 4 young (16384K), 0 survivors (0K)

看 committed 与 used 的差距,以及 young 区大小。libraryd 场景:容器内存告警时,确认堆上限是否按容器设置;MaxRAMPercentage 没配时,JVM 可能按宿主机内存计算。

jmap -histo 与 GC.class_histogram

jmap -histo:live <pid> | head -15
jcmd <pid> GC.class_histogram | head -10

本机 jmap 输出:

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:           200         468800  jdk.internal.vm.StackChunk (java.base@25.0.5)
   2:          8813         397928  [B (java.base@25.0.5)
   3:          1872         246120  java.lang.Class (java.base@25.0.5)
   4:          8730         209520  java.lang.String (java.base@25.0.5)
   9:           200          33600  java.lang.VirtualThread (java.base@25.0.5)

StackChunk 和 VirtualThread 各 200 个,与 200 个睡眠虚拟线程对应。libraryd 场景:内存涨但堆 dump 里看不到业务对象时,先看 StackChunk、Class、byte[];虚拟线程的栈是放在堆上的,数量多时本身就会占内存。

-histo:live 会触发一次 Full GC,生产环境慎用;只想看概况时用 jcmd GC.class_histogram,影响类似。

JFR

命令行启动

java -XX:StartFlightRecording=filename=/tmp/libraryd.jfr,settings=profile,duration=30s \
  -jar libraryd-app.jar

本机用 PinningDemo 验证 synchronized 是否 pin:

java -XX:StartFlightRecording=filename=/tmp/vol3-d/diag/pinning.jfr,settings=profile,dumponexit=true \
  -cp out PinningDemo
jfr print --events jdk.VirtualThreadPinned /tmp/vol3-d/diag/pinning.jfr | wc -l

输出为 0,事件类型在 metadata 中存在,但 200ms 的 synchronized + sleep 没有产生 pin 事件。

运行中启动

jcmd <pid> JFR.start name=short settings=profile duration=30s filename=/tmp/vol3-d/diag/short.jfr
jcmd <pid> JFR.check name=short

本机输出:

Started recording 1. The result will be written to:
/tmp/vol3-d/diag/short.jfr
Recording 1: name=short duration=30s (running)

JFR.check 在录制结束后会提示找不到该名字,这是正常的;要确认是否还在录,趁 duration 未到时执行。

汇总与采样

jfr summary /tmp/vol3-d/diag/diag.jfr
jfr print --events jdk.ExecutionSample /tmp/vol3-d/diag/diag.jfr | head -40

本机 summary 节选:

 Version: 2.1
 Chunks: 1
 Duration: 10 s
 Event Type                              Count  Size (bytes)
 jdk.ThreadSleep                           185          3515
 jdk.ExecutionSample                        41           ...

本机 ExecutionSample 节选:

jdk.ExecutionSample {
  sampledThread = "libraryd-worker" (javaThreadId = 25)
  state = "STATE_RUNNABLE"
  stackTrace = [
    DiagDemo.lambda$main$1() line: 22
    java.util.concurrent.ThreadPoolExecutor.runWorker(...) line: 1090
  ]
}

libraryd 场景:接口 P99 升高时,先看 ExecutionSample 里 libraryd 自己的帧占比,再看 ThreadSleep、JavaMonitorEnter、SocketRead 事件。不要只看 CPU 采样就断定「没有热点」;阻塞型问题在 CPU 采样里可能完全看不见。

字节码与依赖

javap

javap -p -c out/com/plume/library/domain/Book.class

本机输出节选:

public final class com.plume.library.domain.Book extends java.lang.Record {
  private final java.lang.String isbn;
  private final java.lang.String title;
  private final java.lang.String author;
  private final int year;

  public com.plume.library.domain.Book(java.lang.String, java.lang.String, java.lang.String, int);
    Code:
       0: aload_0
       1: invokespecial #1   // Method java/lang/Record."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #7   // Field isbn:Ljava/lang/String;

libraryd 场景:确认 record 的规范构造器、equals/hashCode/toString 是否由编译器生成;record 的规范构造器参数名由编译器强制保留,排查普通类反射拿到 arg0 时才需要检查编译是否带了 -parameters。

jdeps

jdeps -s out
jdeps --module-path . --module com.plume.library.domain

本机输出:

out -> java.base
com.plume.library.domain
   requires mandated java.base (@25)
com.plume.library.domain -> java.base

libraryd 场景:模块化迁移前,用 jdeps 找对 JDK 内部 API 的依赖;jdeps --jdk-internals 会列出 sun.misc.Unsafe 一类的使用点。第三方自动模块在输出里通常显示为自动模块名,不能直接当成稳定模块名写进 requires。

运行时裁剪

jlink --add-modules java.base --strip-debug --no-header-files --no-man-pages \
  --output /tmp/vol3-d/mini-jre
/tmp/vol3-d/mini-jre/bin/java -version

本机输出:

openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)

只含 java.base 的运行时约几十 MB,但 libraryd 需要 java.sql、java.net.http、jdk.crypto.ec 等模块,实际裁剪要用 jdeps --print-module-deps 生成模块列表。jlink 只能裁剪 JDK 模块;应用模块要先打成 modular jar,再用 --module-path 加进去。

启动参数

–enable-preview

javac --release 25 --enable-preview -d out-preview StructuredDemo.java
java --enable-preview -cp out-preview StructuredDemo

本机输出:

dashboard=Dashboard[memberId=M1, activeLoans=[B1, B2], overdueCount=1, totalFineCents=350]
timeout=java.util.concurrent.StructuredTaskScope$TimeoutException elapsedMs=58

编译时漏掉 --enable-preview 的报错:

错误: StructuredTaskScope 是预览 API,默认情况下处于禁用状态。
  (请使用 --enable-preview 以启用预览 API)

libraryd 场景:预览特性只能出现在实验分支,构建脚本要把 --enable-preview 同时加到编译和运行;发布产物的启动脚本漏参数,会在类加载阶段直接失败。

–enable-native-access

java -cp out NativeDemo
java --enable-native-access=ALL-UNNAMED -cp out NativeDemo

不加参数的本机输出:

WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by NativeDemo in an unnamed module (file:/private/tmp/vol3-d/diag/out/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

strlen(libraryd)=8

加上参数后只剩 strlen(libraryd)=8。libraryd 场景:FFM 调用 libisbn 的启动脚本统一加 --enable-native-access=ALL-UNNAMED;模块化后改成 --enable-native-access=com.plume.library.app。这是 JEP 472 的告警,不要等到后续版本拒绝才补。

-Xlog

java -Xlog:gc*:file=/tmp/vol3-d/diag/gc.log -cp out NativeDemo

本机输出节选:

[0.025s][info][gc,init] CardTable entry size: 512
[0.025s][info][gc     ] Using G1
[0.026s][info][gc,init] Version: 25.0.5+8-LTS (release)
[0.026s][info][gc,init] CPUs: 10 total, 10 available
[0.026s][info][gc,init] Memory: 32768M
[0.026s][info][gc,init] Heap Max Capacity: 8G

libraryd 场景:容器里怀疑堆设置不对,用 -Xlog:gc*:file=...:time,uptime,level,tags 留下带时间戳的 GC 日志;排查类加载用 -Xlog:class+load=info,排查 safepoint 用 -Xlog:safepoint。日志要滚动,不要无限写满磁盘。

一页式命令表

症状命令看什么
进程是否在跑jps -lv主类、jar、JVM 参数
启动参数对不对jcmd <pid> VM.command_line-D、-Xmx、预览/native 参数
请求卡住jcmd <pid> Thread.printBLOCKED、WAITING、锁地址
虚拟线程卡住jcmd <pid> Thread.dump_to_file -format=jsonvirtual=true 的线程与栈
内存高jcmd <pid> GC.heap_infocommitted/used、young 区
谁占内存jmap -histo:live <pid>实例数、字节数、StackChunk
CPU 高jcmd <pid> JFR.start ... + jfr printExecutionSample 热点帧
方法编译成什么javap -p -c构造器、invokedynamic、桥方法
依赖了哪些模块jdeps -s、jdeps --jdk-internalsjava.base、JDK 内部 API
裁剪运行时jlink --add-modules模块列表、启动验证
预览 API 失败javac/java --enable-preview编译与运行参数成对
原生访问告警--enable-native-accessJEP 472 告警原文

未在本机执行的命令

以下命令只作为排查选项,本附录没有运行,不提供输出:

  • jcmd <pid> GC.heap_dump、jmap -dump:format=b,file=...:堆转储,磁盘和停顿成本高。
  • jcmd <pid> VM.native_memory summary:需要启动时加 -XX:NativeMemoryTracking=summary。
  • async-profiler:本机未安装,不作为实测命令;需要火焰图时优先用 JFR + JMC。
  • jcmd <pid> Thread.print -l 的完整锁链:本机只验证了基础 Thread.print,-l 输出未逐条摘录。

小结

  • 先分层再选命令:进程、线程、堆、采样、字节码、依赖、运行时,不要一上来就抓全量 dump。
  • jps -lv 和 jcmd VM.command_line 用一分钟确认「跑的是不是你以为的那个进程」。
  • 平台线程看 Thread.print/jstack,虚拟线程必须用 Thread.dump_to_file -format=json。
  • 内存先看 GC.heap_info,再看 jmap -histo;虚拟线程的 StackChunk 会出现在直方图里。
  • JFR 适合定位阻塞和采样热点,jfr summary 先看事件分布,再 jfr print 看具体栈。
  • javap、jdeps、jlink 分别回答「编译成什么」「依赖什么」「裁完能不能跑」。
  • 预览参数必须在编译与运行两侧成对出现;原生访问参数只加在运行侧,JEP 472 的告警不是永久状态。
  • 没跑过的命令就标注未执行,不要用记忆补输出。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发