状态:草稿 (2026-10-26),已反映实测结果。 实测环境有两个。① 在笔记本电脑的容器(预先下载的 eclipse-temurin:21 镜像,JDK 21.0.12,无网络)中亲自动手制作并运行了五个示例程序。没有重新下载或安装任何东西。② 在 TT Lab 集群中运行的 Keycloak Pod(Red Hat 构建的 OpenJDK 21.0.11)上执行了 仅读取命令(
jcmd 1 VM.version类的操作。没有在生产 Pod 中打开调试端口、启动 JFR 记录、生成堆转储或重启。`async-profiler` 需要下载,故为 未实测。官方文档于当天查阅了原文进行核实。 编写规则:以实测数据为基础,未能测量的内容标注为「未实测」,无法确认的内容标注为「无法确认」,推测标注为「推测」。
目录
- 1. 三种观察方法
- 2. 深入 JVM 内部的两条途径
- 3. 使用调试器断点调试:`jdb` 初体验
- 4. 如何对 Kubernetes Pod 进行远程调试
- 5. 不停机观察:`jcmd`
- 6. 案例 1:暂停与死锁
- 7. 案例 2:高 CPU 占用
- 8. 案例 3:内存持续增长
- 9. JFR:常驻开启的飞行记录
- 10. 生产 Pod 中的操作顺序与注意事项
- 11. 总结
- 参考资料
1. 三种观察方法
当 Java 服务出现异常时,通常可以采取以下三种方法。
- 断点调试。 使用调试器暂停执行并逐行推进以查看变量。虽然它能提供最丰富的信息,但在暂停期间服务也会停止。这是开发环境专用的工具。
- 不停机观察。 附加到正在运行的 JVM 上,瞬间抓取线程、堆和配置信息(`jcmd`、`jstack`)。这是生产环境中使用的首要工具。由于某些命令会极短暂停顿 JVM,因此必须了解其影响。
- 记录后观察。 稍后读取常驻开启的轻量级记录(GC 日志、JFR)。诸如「为什么昨天凌晨 3 点系统变慢了」这种已经过去的问题,只能通过这种方法来排查。
本文首先通过图示了解 JVM 与外部工具通信的两条途径(第 2 节),随后依次介绍调试器(第 3-4 节)、诊断命令(第 5 节)、三个案例(第 6-8 节)、JFR(第 9 节)以及生产环境中的操作顺序与注意事项(第 10 节)。
2. 深入 JVM 内部的两条途径
调试器的途径:JPDA。 Java 平台调试架构(JPDA)分为三层(官方文档)。
| 层 | 位置 | 职责 |
|---|---|---|
| JVM TI | 目标 JVM 内部的原生接口 | JVM 提供调试服务,如“暂停此线程”、“在此方法设置断点”、“获取局部变量值” |
| JDWP | 通信协议 | 调试器与目标 JVM 之间的二进制协议,通过套接字或共享内存 |
| JDI | 调试器端 Java API | IDE 和 `jdb` 使用的高级接口 |
-agentlib:jdwp=… 在目标 JVM 中加载 JDWP 代理(原生库),调用 JVM TI 并通过 JDWP 进行通信。将其常用的命令行展开如下(JDK 21 文档)。
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
│ │ │ └ 어디서 기다리나. 호스트를 비우면 루프백만, * 면 모든 인터페이스
│ │ └ 시작하자마자 멈추고 디버거를 기다릴까(기본 y!)
│ └ JVM 이 듣는 쪽(서버)이 될까(기본 n: JVM 이 디버거로 걸어 나간다)
└ TCP 소켓
suspend 的默认值是 y ,如果忘记这一点,仅添加调试选项启动的服务 将在调试器连接之前完全无法启动。 如果是 Kubernetes,就绪探针将持续失败。该地址在 JDK 9 中发生了更改。过去,只需提供端口即可在所有接口上监听,但从 JDK 9 开始,它仅在回环地址上监听,如果需要监听所有接口,则必须附加 *: 。JDK 9 发行说明中提到,绑定到 * 是「不安全且不推荐的」。
诊断命令的途径:Attach。 jcmd·jstack·jmap 不使用网络。它会连接到目标 JVM 临时目录中的 Unix 套接字(.java_pid<PID>),JVM 在收到第一个请求时会启动一个名为 Attach Listener 的线程来执行命令。这仅限于同一用户、同一主机(如果是容器则为同一容器)。因此,在 Kubernetes 中,需要通过 kubectl exec 进入该容器内部执行。
3. 使用调试器断点调试:`jdb` 初体验
编写了一个每秒钟对某项得分进行一次计分的的小程序(Rally.point(boolean leftWins, int rallyNo)),使用 -g 进行了编译,在开启 JDWP 的情况下启动,然后从同一容器中通过 `jdb` 进行连接(2026-10-26)。
$ jdb -attach 5005 -sourcepath /w
> stop in Rally.point
Breakpoint hit: "thread=main", Rally.point(), line=6 bci=0
6 if (leftWins) left++; else right++;
main[1] where
[1] Rally.point (Rally.java:6)
[2] Rally.main (Rally.java:13)
main[1] locals
Method arguments:
leftWins = false
rallyNo = 6
main[1] print Rally.left ← 정적 필드는 클래스 이름을 붙여야 한다(그냥 left 는 「Name unknown」)
Rally.left = 4
main[1] set leftWins = !leftWins
leftWins = !leftWins = true
main[1] clear Rally.point
main[1] cont
结合时间戳查看程序端的输出,可以清楚地了解调试器所做的事情。
| 时间(秒) | 输出 | 说明 |
|---|---|---|
| :34.03 | 回合 5 → 4:1 | 按常态 1 秒间隔 |
| :39.54 | 回合 6 → 5:1 | 在断点处 暂停了 4.5 秒,并将原本应为右侧得分(4:2)的回合改为了左侧得分 |
| :42.05 | Listening for transport dt_socket at address: 5005 |
结束 `jdb` 时 恢复监听 |
调试器不仅可以暂停执行,还能 修改变量值。 并且,即使调试器离开,端口依然在等待下一个访客。这两个事实导致了下一节中的安全问题。
IDE 远程调试的原理也是相同的。IntelliJ 的「Remote JVM Debug」和 VS Code 的 "request": "attach" 配置会在上述 `jdb` 的位置通过 JDI 连接到相同的 JDWP 端口。
调试端口没有认证
JDWP 连接始于双方交换 JDWP-Handshake 14 字节数据。这就是全部。经过了亲自验证。连接到端口并发送了 14 字节,然后发送了一个请求获取「VM 版本」的命令数据包(11 字节)。
JDWP-Handshake
…Java Debug Wire Protocol (Reference Implementation) version 21.0
JVM Debug Interface version 21.0
JVM version 21.0.12 (OpenJDK 64-Bit Server VM, mixed mode, sharing)…
既没有询问密码,也没有询问证书。这意味着任何能够触及该端口的人都可以执行调试器能做的事情(调用任意方法、修改值)。虽然没有找到明文规定 JDWP 没有认证的官方文档语句(无法确认),但上述实测结果、JDK 9 发行说明中的警告以及 OpenJDK 邮件列表中关于「允许远程代码执行」的说明都指向同一方向。在实测容器中, address=*:5005 的套接字在 /proc/net/tcp6 的所有地址(::)的 5005 端口上处于运行状态。
开销如何? 如果没有连接调试器,仅仅加载 JDWP 代理的开销小到无法测量(第 9 节表格:较基准 −0.4%,在噪声范围内)。正因如此,它「嫌麻烦懒得关」而很容易留在生产镜像中,这也正是其危险所在。
4. 如何对 Kubernetes Pod 进行远程调试
原则上不应在生产 Pod 中进行。如果断点暂停了服务线程,请求就会在此期间堆积,导致存活探针(liveness)失败,进而被 kubelet 杀死容器。在开发或测试环境中进行时的安全做法如下:
- 单独创建调试副本。 不要修改生产 Deployment,而是使用相同的镜像创建一个专门用于调试的 Pod(
kubectl debug <파드> --copy-to=<사본>或单独的清单)。如果在环境变量JAVA_TOOL_OPTIONS中加入 JDWP 选项,JVM 就会自动读取,无需修改镜像的启动脚本。 - 仅绑定到回环地址。
address=127.0.0.1:5005配置为仅在 Pod 内部的回环地址监听(如 ),且不为 Pod 外部创建 Service。 - 通过 port-forward 仅暴露给本地电脑。
kubectl port-forward pod/<사본> 5005:5005默认仅在本地电脑的localhost上打开(官方文档:--address默认值为 localhost),且仅支持 TCP。由于它需要经过 API 服务器的认证与授权(RBAC 的pods/portforward),相当于由 Kubernetes 代为完成了 JDWP 本身所不具备的认证。 - 关闭或延长探针。 在副本中移除 liveness 探针,并将 IDE 的断点设置为仅暂停「该线程」而非「所有线程」。
- 结束后删除副本。
本节的步骤遵循本文不在生产 Pod 中打开调试端口的原则,故为 未实测。关于 port-forward 可以连接到 Pod 内部回环地址这一点,仅通过 Kubernetes 文档中的示例(转发到 Pod 端口)进行了确认。
5. 不停机观察:`jcmd`
jcmd <PID> help 展示了该 JVM 接收的所有命令。在此一并记录了常用命令及其 对 JVM 造成的负担(负担根据实测和文档进行判断)。
| 命令 | 显示内容 | 负担 |
|---|---|---|
VM.version · VM.uptime · VM.flags |
版本、运行时间、实际生效的 JVM 选项 | 几乎没有 |
Thread.print (= jstack) |
所有线程的栈、锁及死锁检测 | 短暂暂停(safepoint)。实测 89ms(命令完整往返) |
GC.heap_info |
各堆区域使用量 | 几乎没有 |
GC.class_histogram |
按类统计的对象数及字节数 | 会触发 Full GC(第 8 节实测) |
GC.heap_dump <파일> |
将整个堆导出为 HPROF 文件 | JVM 暂停,占用与堆大小相等的磁盘空间 |
JFR.start · JFR.dump · JFR.stop |
开始、保存、结束飞行记录 | 默认设置下约 1% 左右(第 9 节) |
VM.native_memory summary |
堆外内存(线程栈、元空间等) | 仅在启动时开启了 -XX:NativeMemoryTracking 时有效 |
Thread.dump_to_file |
包含虚拟线程在内的转储(支持 JSON) | 见于 JEP 444 示例,JDK 21 `jcmd` 手册中没有,但在 JDK 25 手册中 |
实测:容器内的 JVM 如何识别内存
JVM 会读取容器的内存限制(cgroup)来决定默认堆大小。默认值为该限制的 25%(-XX:MaxRAMPercentage 默认值,官方文档)。我们为笔记本容器分配了内存限制,并通过 -XX:+PrintFlagsFinal 读取了最大堆内存。
| 容器内存 | 无选项 | -XX:MaxRAMPercentage=70 |
|---|---|---|
| 512MiB | 128MiB | 360MiB |
| 2GiB | 512MiB | 1,434MiB |
如果在无任何选项的情况下将其运行在 2GiB 的 Pod 中,堆内存将仅有 512MiB,其余 1.5GiB 将被闲置。反之,如果将比例调得太高,堆外内存(线程栈、元空间、直接缓冲区)将不足,导致容器被 `OOMKilled`。此时既不会产生 Java 的 `OutOfMemoryError`,也不会生成堆转储,Pod 会直接消失。
实测:仅读取集群的 Keycloak Pod
通过 kubectl exec 进入 TT Lab 集群的 Keycloak Pod 并执行了 `jcmd`(2026-10-26)。该镜像使用的是 Red Hat 的 headless JRE,其中也包含 jcmd (没有jstack·jmap )。由于一个 `jcmd` 就能干两个工具的活,因此没有任何问题。该 JVM 的 PID 为 1。
| 命令 | 结果 |
|---|---|
VM.version |
OpenJDK 64-Bit Server VM 21.0.11+10-LTS |
VM.uptime |
711,384秒(约 8.2 天) |
VM.flags(部分) |
MaxRAM=2147483648(Pod 限制为 2GiB)、 MaxRAMPercentage=70 → MaxHeapSize=1503657984(1,434MiB)、 UseG1GC, ExitOnOutOfMemoryError, FlightRecorderOptions=stackdepth=512 |
GC.heap_info |
G1 堆预留 177MiB,已使用 157MiB,Metaspace 141MiB |
Thread.print 摘要 |
95 个线程(处于 Java 状态的有:RUNNABLE 40、TIMED_WAITING 15、WAITING 14)。无死锁。最多的是 vert.x-eventloop-thread-N 22 个 |
JFR.check |
无正在进行的记录 |
VM.native_memory summary |
「Native memory tracking is not enabled」 |
读取到几项内容。由于镜像开启了 ExitOnOutOfMemoryError ,因此如果堆溢出,JVM 会立即终止,由 Kubernetes 重新启动。不过,并没有留下原因分析的堆转储选项。最大堆为 1,434MiB,而实际使用量仅为 157MiB,因此余量非常充足。还可以确认,上表中 2GiB 与 70% 的计算结果(1,434MiB)与生产 Pod 的值完全一致。
请记住,每执行一条命令,就会创建一个 Attach Listener 线程。即使是「读取命令」,也是在目标 JVM 内部创建一个线程的工作。
6. 案例 1:卡顿与死锁
运行一个两个线程以相反顺序获取两个锁的经典示例(rally-A 为 table → net, rally-B 为 net → table),并在 2 秒后拍摄了 jstack 1 。
Found one Java-level deadlock:
=============================
"rally-A":
waiting to lock monitor 0x0000ffff18001d30 (object 0x000000008bb13c80, a java.lang.Object),
which is held by "rally-B"
"rally-B":
waiting to lock monitor 0x0000ffff1c000f00 (object 0x000000008bb13c70, a java.lang.Object),
which is held by "rally-A"
…
"rally-A":
at Deadlock.grab(Deadlock.java:10)
- waiting to lock <0x000000008bb13c80> (a java.lang.Object)
- locked <0x000000008bb13c70> (a java.lang.Object)
…
Found 1 deadlock.
JVM 会自行找出环路(形状与官方故障排除指南中的示例相同)。两个线程的 CPU 时间分别为 1.01ms 和 0.60ms,这也表明卡住的服务并没有消耗 CPU。在现实中,卡顿比死锁更常见的情况是 等待某个慢速操作(如数据库连接池耗尽、外部 HTTP 调用没有超时设置)。因此,转储通常会 以 5–10 秒的间隔拍摄三次 ,以观察相同的线程是否持续停留在同一行。仅凭单张快照,无法判断它是否只是刚好在那一瞬间停留在此处。
7. 案例 2:CPU 使用率过高
这是一个示例,其中一个线程(referee-worker)在进行计算,而其他四个线程处于休眠状态。顺序是 哪个线程 → 该线程在做什么。
- 在容器内部使用
top -H -p 1查看每个线程的 CPU 情况。实测结果:TID 24 占比 90.9%,名称为referee+(被截断)。 jstack 1在nid中查找该 TID。线程行中的
"referee-worker" #20 [24] … cpu=3922.52ms elapsed=3.96s … nid=24 runnable
java.lang.Thread.State: RUNNABLE
at Hot.scoreRally(Hot.java:7)
就是 OS 线程编号。 nid=24 实测使用的 JDK 21 像 一样用十进制 [24] 记录,并同时显示 nid=0x18 。JDK 8 和 11 时代的指南使用的是十六进制(如 ),要求将 TID 转换为十六进制再进行查找,因此需要根据对应的版本进行读取。
- 单张线程栈只代表一个瞬间。如果要了解「大部分时间花在了哪里」,可以使用 JFR。在运行中的 JVM 上 无需重启 即可开始记录。
$ jcmd 1 JFR.start name=hot duration=10s filename=/w/hot.jfr
$ jfr view hot-methods /w/hot.jfr
Method Samples Percent
Hot.scoreRally(long) 460 100.00%
在 10 秒内收集的 460 个执行样本(默认设置约为 20ms 间隔)全部指向同一个方法。同一记录中的 jfr view cpu-load 显示 JVM 用户态 CPU 平均为 9.02%,但这 是相对于整台机器(10 个核心)而言的 数值,其含义相当于「跑满了一个核心」。务必确认百分比所对应的基数是什么。
async-profiler(最新 v4.6)是一个利用 JVM 内部 API(AsyncGetCallTrace)和 Linux perf_events 的工具,它能够无 safepoint 偏向地同时捕获 Java 和本地线程栈(详见官方 README)。它能直接生成火焰图,对排查 CPU 问题特别有效,但由于需要下载并放入 Pod 中,因此本次 未实测。
8. 案例 3:内存持续增长
我们启动了一个最大堆为 64MB 的服务,其中包含一个只往列表中添加 64KB 帧却从不移除的「缓存」。相关选项采用了生产环境中推荐的组合。
java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/w/heap.hprof \
-XX:+ExitOnOutOfMemoryError "-Xlog:gc*:file=/w/gc.log:uptime,level,tags" -cp classes Leak
-Xlog:gc* 是由 JDK 9 的统一日志(JEP 158)与统一 GC 日志(JEP 271)结合而形成的的格式。结果如下。
| 时间 | 标准输出 / GC 日志 |
|---|---|
| 1.9 秒 | Pause Young (Normal) 23M->22M(64M) 8.5ms — 清空年轻代,但几乎没有减少 |
| 2.3 秒 | Pause Full (Heap Inspection Initiated GC) 27M->26M — 我执行的 GC.class_histogram 引发的 Full GC |
| 3.2–5.3 秒 | Young 与 Concurrent Mark 交替进行,使用量从 36M 持续上升至 59M |
| 5.45–5.55 秒 | Evacuation Failure,接着是三次 Pause Full (G1 Compaction Pause) 59M->59M — 即使执行 Full GC,连一个字节都没有减少 |
| 5.5 秒 | OutOfMemoryError: Java heap space,以 0.073 秒生成 65,304,426 字节的堆转储,退出码 3 |
读取方法只有一个: 如果在 Full GC 后使用量仍未下降,说明有存活的对象正在死死抓住内存不放。 这是区分单纯的分配过多与内存泄漏的标准。在 2 秒时的类直方图甚至显露出了罪魁祸首的模样: [B(字节数组)9,728 个,共 26,419,600 字节,排名第一。不过,实测记录也表明该命令触发了 Full GC(2.3 秒处)。在拥有大堆的生产环境中,这绝不是一条可以「轻描淡写看一眼」就能随便执行的命令。
在堆转储中查找「究竟是谁抓住了这些数组」(支配树)需要使用 Eclipse MAT 或 VisualVM 等分析工具,因此本次 未实测。从实测中确认的生产环境事实有两点:转储文件 与堆大小相当(64MB 堆对应 65MB 转储),因此如果是 1.4GB 堆的 Pod,就需要相应大小的可写卷。此外,转储文件中包含了内存中的所有内容,即用户信息、令牌和密码,因此必须像处理机密数据一样谨慎对待。
9. JFR:常驻开启的黑匣子
JFR(JDK Flight Recorder)在 JDK 11 中成为了开源项目(JEP 328),其默认设置的目标开销在 1% 以下。JDK 21 的 default.jfc 自述「在生产环境中持续使用也很安全,通常低于 1%」,而 profile.jfc 则自述「通常在 2% 左右」。为了验证是否确实如此,我们运行并测量了包含哈希计算和短生命周期对象分配的混合任务固定次数(400 万次)。四种设置交替运行 7 次,并绑定到 2 个容器 CPU 上。
| 设置 | 主循环时间中位数(7 次) | 相比基准的比例 |
|---|---|---|
| 基准(无选项) | 1,734ms | — |
JFR default |
1,750ms | +0.9% |
JFR profile |
1,753ms | +1.1% |
| 仅 JDWP 代理(未附加调试器) | 1,727ms | −0.4% |
由于基准 7 次的范围在 1,713–1,784ms 之间,本身就有约 ±2% 的波动,因此上述差异只能说明「在文档标称的 1%·2% 范围之内」。由于它会根据任务的性质(分配极多、锁竞争激烈等)而有所不同,建议在您自己的服务上亲自测量一次。
JFR 文件中包含环境变量
上述实测记录的 jfr summary 中包含了 11 条 jdk.InitialEnvironmentVariable 和 15 条 jdk.InitialSystemProperty ,使用 jfr print 打开后, 键和值会原封不动地显示出来 。Kubernetes Pod 经常通过环境变量接收数据库密码或 API 密钥,因此在将 JFR 文件附加到工单或发送到外部之前,必须将其擦除。JDK 21 的 jfr scrub 正是用于此项工作。
$ jfr scrub --exclude-events jdk.InitialEnvironmentVariable,jdk.InitialSystemProperty hot.jfr hot-clean.jfr
实测:清理后的文件中上述两个事件归零,而 460 个执行样本保持不变(大小从 211,772 字节变为 201,397 字节)。
在生产环境中使用 JFR 的形态
- 常驻开启:在启动选项中像
-XX:StartFlightRecording=settings=default,maxage=6h,disk=true,filename=/tmp/jfr/一样设定时间进行环形循环。在发生问题后,通过jcmd 1 JFR.dump提取过去几小时的数据。这是查看「过去发生的事情」的唯一方法。 - 按需开启:如第 6–7 节所述,通过
jcmd 1 JFR.start duration=60s在不重启的情况下开启。 - 读取:
jfr summary,jfr view hot-methods·gc·allocation-by-class,或者使用 JDK Mission Control 打开。
10. 生产 Pod 中的排查顺序与注意事项
按症状分类的优先查看项总结如图 2 所示。顺序的原则是 从开销最小的开始。
- 现成留存的日志:应用日志、GC 日志、指标、常驻开启的 JFR。切勿修改任何内容。
- 轻量级读取:
VM.flags,GC.heap_info,VM.uptime。在此首先排除堆大小设置错误的情况(第 5 节)。 - 短暂暂停:
Thread.print带着时间间隔拍摄三次。 - 记录:
JFR.start duration=…. - 重型操作:堆转储、类直方图。应将流量引流到其他 Pod(将其从就绪状态中剔除),或者配置
-XX:+HeapDumpOnOutOfMemoryError以便仅在 OOM 发生的瞬间自动触发。
此外,应避免以下做法。
- 切勿在生产镜像或清单中残留 JDWP 选项。特别是
address=*:…与暴露 Service 属于远程代码执行的通道。 - 堆转储和 JFR 文件应作为包含机密数据的档案来处理。当需要外发时,JFR 需经过
scrub处理,而堆转储原则上禁止外发。 - 同时确定容器内存限制与
MaxRAMPercentage。默认的 25% 太少,而 90% 则会饿死堆外内存。 - 请务必确认「CPU 9%」之类数字所代表的基准。10 核机器上的 9% 意味着跑满了一个核心。
11. 总结
- 调试器(JPDA)分为 JVM TI、JDWP 和 JDI 三层,其中
-agentlib:jdwp打开了它的入口。该入口没有任何认证。实测表明,仅凭 14 字节的握手就能获取 VM 信息,且即便调试器断开,端口也会重新开放。在 Kubernetes 中,应当通过回环地址(loopback)结合 port-forward,置于 API Server 的认证之后。 - 生产环境的首选工具是 jcmd。它通过同一容器内的 Unix 套接字连接,每条命令的开销各不相同。实测表明,类直方图会触发 Full GC。
- 三种案例的应对首选:卡顿使用
Thread.print三次,CPU 使用top -H→nid→ JFR,内存则检查「Full GC 后是否依然不降」。 - JFR 的默认设置实测开销在 1% 以内,且无需重启即可开启。文件中会残留环境变量,因此必须通过
jfr scrub处理后再导出。
Titi 的寄语。 如果用调试器暂停了拉力赛,球、对手和观众都会停下来。比赛期间最好只用 jcmd 拍个照,录像(JFR)则常驻开启为好。啊,对了,录像里拍到了更衣室密码(环境变量),分享前可一定要擦除哦!
参考
- JPDA 架构 https://docs.oracle.com/en/java/javase/21/docs/specs/jpda/architecture.html · JDWP 连接与调用选项(JDK 21) https://docs.oracle.com/en/java/javase/21/docs/specs/jpda/conninv.html
- JDK 9 发行说明(JDWP 默认绑定到 localhost,
*被标记为「不安全且不推荐」,JDK-8041435) https://www.oracle.com/java/technologies/javase/9-notes.html - OpenJDK discuss 邮件列表(2025-01-20,提及 JDWP 的远程代码执行风险,非官方文档) https://mail.openjdk.org/pipermail/discuss/2025-January/006502.html
- jcmd 手册 JDK 21 https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html · JDK 25 https://docs.oracle.com/en/java/javase/25/docs/specs/man/jcmd.html · JEP 444(虚拟线程,Thread.dump_to_file) https://openjdk.org/jeps/444
- java 命令手册 JDK 21(MaxRAMPercentage 默认 25%、HeapDumpOnOutOfMemoryError、StartFlightRecording) https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html · 8u92 发行说明(ExitOnOutOfMemoryError) https://www.oracle.com/java/technologies/javase/8u92-relnotes.html
- JEP 158 统一 JVM 日志记录 https://openjdk.org/jeps/158 · JEP 271 统一 GC 日志记录 https://openjdk.org/jeps/271 · JEP 328 Flight Recorder https://openjdk.org/jeps/328
- jfr 工具手册 https://docs.oracle.com/en/java/javase/21/docs/specs/man/jfr.html · default.jfc(JDK 21u) https://raw.githubusercontent.com/openjdk/jdk21u/master/src/jdk.jfr/share/conf/jfr/default.jfc
- 故障排查指南:挂起与循环(死锁报告示例,JDK 17) https://docs.oracle.com/en/java/javase/17/troubleshoot/troubleshoot-process-hangs-and-loops.html
- async-profiler https://github.com/async-profiler/async-profiler
- Keycloak 容器 Dockerfile(UBI9-micro, java-21-openjdk-headless) https://github.com/keycloak/keycloak/blob/main/quarkus/container/Dockerfile
- Kubernetes port-forward https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/ · https://kubernetes.io/docs/reference/kubectl/generated/kubectl_port-forward/
- 所有原文均于 2026-10-26 打开确认过。