JVM 综合诊断
JVM 诊断先确认目标进程是否存活,再选择能够解释当前问题的工具。线程栈记录某一时刻的执行位置,JFR 记录一段时间内的事件,heap dump 保存对象图,NMT 则跟踪它所覆盖的 native 内存分类。
CPU 高、请求慢、Full GC 和内存上涨可能互相影响。把它们按发生顺序对齐,再检查具体线程、对象或运行时组件,才能确定应该修复哪里。采集本身也消耗 CPU、内存、磁盘和暂停时间,因此每条命令都应有明确用途。
确认进程、用户与故障窗口
实验环境为 amd64/x86_64 Temurin HotSpot 17.0.20+8。java、jcmd、jfr 等工具应来自目标 JVM 的同一 JDK 发行线;用另一条 JDK 版本的工具附加,命令集合和输出字段都可能变化。
进程仍在和进程已退出,是第一处分支:
| 现场 | 还能取得什么 | 不应继续假设什么 |
|---|---|---|
| JVM 仍存活 | 版本、启动参数、线程栈、堆概况、既有 JFR,预启用时还有 NMT | 不能因为 attach 成功就把高影响命令当成免费操作 |
| Java OOM 后仍存活 | OOM detail message、GC 日志、有限 attach 证据 | OOM 一定来自 Java heap |
容器 OOMKilled 或进程已退出 | 容器事件、退出码、应用/GC 日志、事先留下的 JFR、HPROF、hs_err_pid | PID、/proc/$PID 和 jcmd 仍然存在 |
| JVM fatal error | hs_err_pid、core、系统日志、退出前 JFR | heap dump 一定会生成 |
如果进程仍在,先在目标主机或容器内设置明确的变量。jcmd 通常要求与目标 JVM 使用相同的操作系统用户,并处在能看到该 PID 的 namespace 中:
export JDK17_HOME=/opt/jdk-17.0.20+8
read -r -p "输入已确认的目标 Java PID: " PID
case "$PID" in
""|*[!0-9]*|0) printf 'PID 无效\n' >&2; exit 1 ;;
esac
ps -o pid,ppid,etime,user,cmd -p "$PID"
readlink -f "/proc/$PID/exe"
"$JDK17_HOME/bin/jcmd" "$PID" VM.version三条结果必须指向同一个仍存活的 Java 进程。容器外看到的宿主机 PID 与容器内 PID 可能不同;不要用 jcmd 0 向所有可见 JVM 广播命令。attach 失败时先核对用户、PID namespace、镜像里是否有完整 JDK,以及启动参数是否禁用了 attach,不要反复重试高影响命令。
Docker 环境要区分宿主机 PID 与容器内 PID。拥有已授权 Docker 访问权限的操作用户,可以先用 docker inspect 检查目标容器,再用 docker stats 读取当前资源统计:
read -r -p "输入目标容器名称或 ID: " CONTAINER
docker inspect --format 'running={{.State.Running}} hostPid={{.State.Pid}} oomKilled={{.State.OOMKilled}} exit={{.State.ExitCode}}' "$CONTAINER"
docker stats --no-stream "$CONTAINER"容器已退出就转向日志与退出状态。仍在运行时,可在容器内部、以 Java 进程所属 UID 调用镜像中的完整 JDK,使用其中看到的 PID。镜像若没有诊断工具,不要临时以 root 安装全套软件;按事先准备的调试镜像或诊断容器流程处理。共享 PID 命名空间仍不代表 /tmp、用户权限和 JDK 版本都一致。
同时记录 T0、告警指标、发布版本、镜像摘要、启动参数和容器限额。下面两条命令影响低,但输出可能含启动参数中的口令或令牌,文件应使用受限权限,不能直接粘贴到公共工单:
umask 077
CASE_DIR=$(mktemp -d /var/tmp/jvm-case.XXXXXX)
"$JDK17_HOME/bin/jcmd" "$PID" VM.command_line > "$CASE_DIR/command-line.txt"
"$JDK17_HOME/bin/jcmd" "$PID" VM.flags > "$CASE_DIR/flags.txt"保留版本和启动参数,后续采样均记录同一实例与时间窗口。若进程中途重启,需要重新确认 PID,不能把前后两次实例的结果直接比较。
选择采集工具
下载完整实验包。Linux amd64/x86_64、Temurin 17.0.20+8 和普通用户是固定实验环境;解压进入 jvm-diagnostics 后,运行自动实验:
export JAVA_HOME=/opt/jdk-17
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
bash run.sh脚本会启动一个带 12 MiB 保留对象、CPU 计算线程与 monitor owner/waiter 的 JVM,取得线程栈、NMT、直方图和 JFR,最后自动停止它。另用独立小堆进程比较无界与有界保留。关键输出为:
thread-evidence=CPU worker present + burnCpu sampled / monitor waiter BLOCKED
memory-negative=32MiB unbounded retention -> Java heap space + HPROF + Full GC
memory-positive=32MiB bounded retention -> 160 blocks attempted / 8 retained / completed
PASS jvm-diagnostics自动脚本结束后,里面的 PID 已不存在。若要练习后面的采集命令,需要另建一个仍在运行的实验目标。
启动可手动检查的目标
以下操作只针对实验包的 DiagnosticTarget.java,不向生产进程注入故障。CPU worker 会持续使用一个核的计算资源;目标最多运行 5 分钟,也可通过 release 文件提前结束。
export JDK17_HOME="$JAVA_HOME"
umask 077
LAB_OUT=$(mktemp -d /tmp/jvm-diagnostics.XXXXXX)
CASE_DIR="$LAB_OUT/evidence"
mkdir "$CASE_DIR"
"$JDK17_HOME/bin/javac" --release 17 -Xlint:all -Werror \
-d "$LAB_OUT" DiagnosticTarget.java
"$JDK17_HOME/bin/java" -Xms64m -Xmx64m -XX:+UseSerialGC \
-XX:NativeMemoryTracking=summary \
-cp "$LAB_OUT" example.jvm.diagnostics.DiagnosticTarget live "$LAB_OUT/release" \
>"$CASE_DIR/target.log" 2>&1 &
PID=$!
for attempt in $(seq 1 200); do
grep -q '^release-file=' "$CASE_DIR/target.log" && break
kill -0 "$PID" 2>/dev/null || break
sleep 0.1
done
cat "$CASE_DIR/target.log"
"$JDK17_HOME/bin/jcmd" "$PID" VM.version
"$JDK17_HOME/bin/jcmd" "$PID" Thread.print -l >"$CASE_DIR/threads.txt"
grep -A15 'diagnostic-' "$CASE_DIR/threads.txt"日志应有对应 PID 和 retained-mib=12,线程栈中应出现 diagnostic-cpu-worker,以及处于 BLOCKED 的 diagnostic-lock-waiter。attach 失败时检查 JVM 是否已到 5 分钟时限、用户是否一致、/tmp 是否可访问;不要改用 jcmd 0 广播尝试。
影响级别与命令帮助
JDK 17 jcmd 文档 会为目标版本的每条命令标出影响级别。实际执行前先询问目标 JVM,而不是照搬别的版本:
"$JDK17_HOME/bin/jcmd" "$PID" help VM.command_line
"$JDK17_HOME/bin/jcmd" "$PID" help Thread.print
"$JDK17_HOME/bin/jcmd" "$PID" help GC.class_histogram
"$JDK17_HOME/bin/jcmd" "$PID" help GC.heap_dump在这条 JDK 17 基线上,常用证据的升级关系如下:
| 证据 | 主要回答 | 官方影响口径与前提 |
|---|---|---|
VM.version、VM.command_line、VM.flags | 目标身份和启动事实 | Low;命令行可能含敏感信息 |
GC.heap_info | 当前 heap 布局和粗粒度占用 | Medium;不是泄漏证明 |
Thread.print -l | 线程状态、Java 栈、monitor 与 ownable synchronizer | Medium;线程越多,成本越高 |
JFR.check、JFR.dump | 已有录制及故障窗口 | Low;必须先有录制,文件仍可能含敏感业务信息 |
VM.native_memory | HotSpot 内部 native 分类 | Medium;必须在启动时开启 NMT,且不覆盖全部 RSS |
GC.class_histogram | 对象数量与浅大小的候选排行 | High;随 heap 大小和内容增长,不能叫“轻量统计” |
GC.heap_dump | 完整对象图、retained heap 与 GC roots | High;默认会请求 Full GC,文件可能巨大且包含业务数据 |
按问题选用其中一项或少数几项即可。一次锁等待可能在一份线程栈中已经清楚;一次短暂 CPU 高峰则更需要覆盖故障窗口的 JFR;进程已经退出时,整张 attach 表都不适用。已有结果足以定位时,不必继续生成高成本文件。
内存异常与进程退出
“内存高”至少有三种口径:Java heap 是 GC 管理的对象区;NMT 统计 HotSpot 自己追踪的 native 类别;进程 RSS 还包含第三方 native 库、部分映射和操作系统实际驻留页面。三者不能互相替代,关系可简化为:
进程驻留内存中的可能组成
├─ Java heap 中已驻留的页面
├─ HotSpot native:Metaspace、Code、Thread、GC 等
├─ direct buffer 与其他 JDK/native 分配
└─ 第三方 native、映射文件及未被 NMT 覆盖的部分进程仍在:先读 OOM detail message
OOM 的异常文本决定第一份证据:
| 现象 | 第一假设 | 第一份区分证据 |
|---|---|---|
Java heap space | 活跃对象超过 heap,或单次分配过大 | GC 日志中的回收后占用、OOM HPROF、引用所有者 |
GC overhead limit exceeded | 大量时间用于 GC,但回收很少 | GC 时间线、回收后 live set、分配速率 |
Metaspace | 类或类加载器生命周期失控 | 类加载数量、Metaspace、loader 统计与引用链 |
Direct buffer memory | direct buffer 预算或释放路径失控 | BufferPool 指标、NMT 旁证、框架自身 allocator 指标 |
unable to create native thread | 线程数量、PID/用户限制或 native 预算不足 | 线程数、/proc/$PID/limits、线程名前缀和栈大小 |
Requested array size exceeds VM limit | 单次数组或集合尺寸异常 | 异常栈、入参、分页/批量尺寸;扩大 heap 通常不解决模型错误 |
Full GC 也不是“heap 太小”的同义词。先在 GC 日志里对齐 event、cause、暂停和回收前后占用:回收后 live set 持续上升,才支持对象长期存活;回收后明显下降但很快再次填满,更像分配速率、批量峰值或容量不匹配。日志读取和收集器机制见 GC 收集器与日志。
heap 正常而 RSS 上涨:NMT 只能解释其中一部分
NMT 默认关闭,事故发生后不能用 jcmd 补开。需要在启动参数中预先选择 summary 或 detail,并在目标负载下评估跟踪开销:
-XX:NativeMemoryTracking=summary进程稳定后建立基线,再比较后续增量。前面的手动实验目标最多存活 5 分钟,练习时使用 10 秒间隔;若已经接近退出时限,先重新启动目标,再沿用新 PID 和目录:
kill -0 "$PID" 2>/dev/null || {
printf '目标进程已退出,请重新确认 PID\n' >&2; exit 1
}
"$JDK17_HOME/bin/jcmd" "$PID" VM.native_memory baseline || exit 1
sleep 10
kill -0 "$PID" 2>/dev/null || {
printf '采样期间目标进程已退出,停止比较\n' >&2; exit 1
}
"$JDK17_HOME/bin/jcmd" "$PID" VM.native_memory summary.diff scale=MB \
> "$CASE_DIR/nmt-T+10s.txt" || exit 1
cat "$CASE_DIR/nmt-T+10s.txt"建立基线应返回 Baseline taken,第二次输出应包含 Native Memory Tracking: 及各类别的 reserved、committed 数据;存在变化时会带有正负增量。实验目标保留的对象数量不变,10 秒内看不到明显增量也属正常。若提示 NMT 未启用、没有基线或 attach 失败,停止比较,先核对启动参数与目标进程;即使存活检查通过,进程也可能在随后执行 jcmd 前退出。
生产采样间隔应覆盖待观察的业务阶段,例如比较预热完成后的相同负载窗口。10 秒只是练习间隔;采用分钟级窗口时,确认目标会持续运行,并记录负载和采样间隔。实例重启后需重新建立基线,不能继续使用旧 PID 或把两次实例的数据作差。
Thread 增长要回到线程数量与栈;Class 增长要回到类加载器;Code 增长要回到 JIT 和 Code Cache。NMT 没有解释的 RSS 差额,不能被硬塞进 Java heap,需要继续检查 direct buffer、第三方 native、映射文件和容器口径。JDK 17 NMT 文档 明确说明它只追踪 HotSpot/JVM 内部使用,并不覆盖所有 native 分配。
进程已退:转向退出制品
容器 OOMKilled 表明运行时将退出归为 OOM kill。需要结合 cgroup memory.events、容器限制和宿主机 OOM 记录,区分容器内存上限与节点级内存压力;退出码 137 单独只表明常见的 SIGKILL 退出形式,也可能来自人工终止。相关计费与事件语义见 Linux cgroup v2。设置明确的 Kubernetes context、namespace 和 Pod 后读取退出状态:
export KUBE_CONTEXT=prod-readonly
export NAMESPACE=payments
export POD=payments-api-0
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD" \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{" current="}{.state.terminated.reason}{"/"}{.state.terminated.exitCode}{" last="}{.lastState.terminated.reason}{"/"}{.lastState.terminated.exitCode}{"\n"}{end}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD" \
--all-containers=true --tail=200 > "$CASE_DIR/current-or-last.log"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--field-selector "involvedObject.name=$POD" > "$CASE_DIR/pod-events.txt"输出中的 current 有终止原因时,第一份日志就是当前已退出容器的日志;last 有终止原因说明容器已经重启,此时再用 kubectl ... logs "$POD" --all-containers=true --previous 读取上一个实例。没有 previous 实例时不要把该命令的失败误写成日志丢失。
如果已经退出,就把容器事件、退出码、应用日志、GC 日志和已有制品放到同一时间线;不要再写依赖 $PID 的命令。Java crash 优先读取 hs_err_pid*.log 和 core;OOM 则读取预先配置的 HPROF。事故前可加入:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/lib/myapp/dumpsdump 目录必须落到持久卷,提前验证可写空间、访问控制、加密、脱敏和保留周期。JDK 17 排障准备指南 也把连续 JFR、GC/应用日志、heap dump 与崩溃文件视为事故前准备,而不是事故发生后临时补救。
存活进程:按需生成对象快照
下面重新进入存活进程分支。确认目标 PID 仍在运行、低影响结果仍不足以定位对象持有者,并且允许暂停与写出文件后,再安排 heap dump:
df -h "$CASE_DIR"
ps -o pid,user,cmd -p "$PID"
umask 077
"$JDK17_HOME/bin/jcmd" "$PID" GC.heap_dump \
"$CASE_DIR/heap-T0-$PID.hprof"默认 heap dump 会请求 Full GC,可能产生长停顿;文件大小受对象数量、布局和记录内容影响,不能用 heap used 精确预测,并可能包含 token、请求体和用户数据。先确认暂停预算和磁盘,再在受控窗口执行。离线分析时,Histogram 只提供候选类;Dominator Tree 的 retained heap 说明谁控制大块对象图,Path to GC Roots 才解释对象为什么仍可达。对象多但能被回收,不等于泄漏。
离线读取对象图
将授权取得的 HPROF 复制到受控分析机,用 Eclipse Memory Analyzer 的基础操作打开。先在 Histogram 中按类型查看数量和浅大小,再进入 Dominator Tree 查找保留大块对象图的对象,最后对候选执行 Path to GC Roots,确认持有路径。分析器内存不足时按对象数量调整其自身容量,不能反推业务 JVM 当时一定内存不足。
Dominator Tree 的父子关系表示支配关系,并非每条边都是一个直接字段引用;这是它与普通引用图的重要差别,详见 MAT 支配树说明。自动 Leak Suspects 报告只给候选,还要核对对象是否已超过业务生命周期。
CPU 热点、锁等待与 JFR
CPU 高时,RUNNABLE 只是 Java 线程状态,不是 CPU 归因。先在 Linux 上连续观察目标进程的线程,取得真正消耗 CPU 的十进制 TID:
pidstat -t -p "$PID" 1 5
read -r -p "输入上述采样中高 CPU 的十进制 TID: " TID
case "$TID" in
""|*[!0-9]*|0) printf 'TID 无效\n' >&2; exit 1 ;;
esac
export HEX_TID="$(printf '%x' "$TID")"
"$JDK17_HOME/bin/jcmd" "$PID" Thread.print -l > "$CASE_DIR/threads-T0.txt"
grep -nE -A35 -B3 "(^|[[:space:]])nid=0x${HEX_TID}([[:space:]]|$)" \
"$CASE_DIR/threads-T0.txt"匹配条件限定了 nid 的完整字段,避免把 0x1234 当作 0x123。带行号和冒号的命中行用于核对线程名,周围带连字符的行只是上下文;没有命中时,先确认线程是否已经退出、TID 与转储是否来自同一进程,再重新采样。
pidstat 由 Linux 的 sysstat 软件包提供;缺少时由具备安装权限的管理员安装,或使用 top -H -p "$PID" 观察。上述 nid 对照适用于平台线程,采样和 jcmd 必须位于能一致识别线程 ID 的命名空间。热点线程停在业务循环、序列化、正则、加解密或日志格式化,才支持“业务代码烧 CPU”;热点是 GC、编译器或 native 线程,就要转入对应机制。高峰已经过去后再抓一份栈,只能证明恢复后的状态。
锁与下游等待需要另一种判断。线程栈里的 BLOCKED 要同时找到等待的 monitor 和持有者;WAITING、TIMED_WAITING 要回到等待对象、线程名前缀、连接池或 Future。一次快照可能撞上正常瞬时状态;故障持续且线程规模允许时,可以间隔采两到三份,确认同一批线程、同一栈位和同一所有者是否稳定出现,而不是机械规定永远抓三份。
JFR 适合覆盖时间窗口。更好的准备是在启动时建立有限大小的循环基线:
-XX:StartFlightRecording=name=baseline,settings=default,disk=true,maxage=1h,maxsize=256m事故中先确认录制存在,再导出最近窗口;JFR.dump 不会停止原录制:
"$JDK17_HOME/bin/jcmd" "$PID" JFR.check name=baseline
"$JDK17_HOME/bin/jcmd" "$PID" JFR.dump name=baseline maxage=2m \
filename="$CASE_DIR/baseline-T0.jfr"
"$JDK17_HOME/bin/jfr" print --events jdk.ExecutionSample \
"$CASE_DIR/baseline-T0.jfr" > "$CASE_DIR/cpu-samples-T0.txt"如果平时没有基线,可以在服务仍有余量且故障可持续时启动一次短 profile 录制:
"$JDK17_HOME/bin/jcmd" "$PID" JFR.start name=cpu \
settings=profile duration=30s filename="$CASE_DIR/cpu-T0.jfr"命令会立即返回,录制要到 30 秒窗口结束后才完成。不要刚执行就读取不存在或未关闭的文件。录制完成后先检查状态,再解析:
"$JDK17_HOME/bin/jcmd" "$PID" JFR.check
test -s "$CASE_DIR/cpu-T0.jfr"
"$JDK17_HOME/bin/jfr" summary "$CASE_DIR/cpu-T0.jfr"
"$JDK17_HOME/bin/jfr" print --events jdk.ExecutionSample \
"$CASE_DIR/cpu-T0.jfr" >"$CASE_DIR/cpu-samples-T0.txt"检查到 cpu 录制仍在运行时,继续等待,不读取未完成文件。在实验目标中,应反复看到 burnCpu 方法的采样。系统 CPU 与 JFR 样本相互支持,但 ExecutionSample 并不覆盖所有 native、内核或 off-CPU 时间;没有 Java 样本时,应继续用相应的系统剖析工具,而非据此排除 CPU 消耗。
在桌面分析机上,也可以用 JDK Mission Control 的 Flight Recorder 视图打开已完成的 JFR,先选故障时间范围,再查看方法采样、线程活动、锁和 GC 事件。界面中某个方法样本占比高,只代表相应采样集合中的比例,解释前先确认事件类型和采样条件。
虚拟线程需要不同的线程视图
JDK 21 起的虚拟线程可在不同 carrier 平台线程之间切换。OS TID 对应 carrier,不能被当成一个虚拟线程的永久身份。JDK 25 可通过 Thread.dump_to_file 取得包含虚拟线程的线程转储;格式、参数与影响以 JDK 25 jcmd为准:
# 只在已确认的 JDK 25 目标上操作,PID 与目录沿用该目标的现场变量
export JDK25_HOME=/opt/jdk-25
"$JDK25_HOME/bin/jcmd" "$PID" help Thread.dump_to_file
"$JDK25_HOME/bin/jcmd" "$PID" Thread.dump_to_file -format=json \
"$CASE_DIR/threads-virtual.json"不要把此命令块接到前面的 JDK 17 实验 PID 上。JDK 24 已改变 synchronized 导致虚拟线程 pinning 的情况,不能照旧把 synchronized 一律判为 carrier 长期占用的原因;native 等路径仍需具体分析。参见 JDK 25 虚拟线程指南。
根据结果进入对应机制
看到下面这些同窗信号时,先进入对应机制专题验证,不在事故现场重新推演整套 JVM:
| 同窗信号 | 更可能的下一条链 | 先去验证什么 |
|---|---|---|
| GC 线程热点、GC pause 覆盖慢请求、回收后 live set 异常 | GC 收集器与日志 | collector、event、cause、回收前后 delta 与余量 |
| 编译线程热点、启动抖动、旧 nmethod 退役或 Code Cache 压力 | JIT、逃逸分析与 Code Cache | 预热、tier/OSR、deoptimization 与 Code Cache 状态 |
| Metaspace 与类数量增长,部署或插件切换后不回落 | 类加载器与委派边界 | defining loader、父层引用、TCCL 与卸载条件 |
| heap、direct 与 RSS 口径混淆 | 运行时内存与对象布局 | reserved/committed/used、对象图与进程预算 |
| 大量线程稳定卡在同一锁、队列、连接池或下游 | 并发诊断 | owner/waiter、队列边界、超时、中断与资源归还 |
时间顺序决定因果方向。若请求流量先升、分配速率随后升、GC 最后变密,GC 可能是受害者;若 GC pause 先覆盖接口延迟,应用线程的积压可能是后果;若发布后类数量和 Metaspace 单调增长,CPU 与 Full GC 可能都只是类加载器滞留的次生现象。把指标、线程栈、JFR、GC 日志和发布记录放在同一个 T0 时间轴上,才能区分“同时发生”和“谁先推动谁”。
临时扩 heap、重启实例、限流或摘流可以止损,但不会自动证明根因。尤其不要因为扩 heap 后告警晚出现,就宣布泄漏已经修复;也不要因为重启释放了 RSS,就把问题命名为某个未被证据指向的类或线程池。
复测与清理
公开实验的内存部分刻意保持 JDK、32 MiB heap、收集器和 160 次分配请求上限不变,只改变保留策略。无界侧在完成前出现 Java heap space,留下 Full GC 日志与临时 HPROF;有界侧只保留 8 个块并完成全部 160 次分配。这个对照证明的是“保留边界改变了活跃对象规模”,不证明所有生产 OOM 都应使用缓存上限解决。
线上修复也要先写出可证伪条件:
| 假设 | 修复前区分证据 | 只改变什么 | 同窗复测通过条件 |
|---|---|---|---|
| 无界缓存扩大 live set | GC 后占用持续升、dump 根路径回到缓存 | 增加容量与过期边界 | 同负载下缓存条目稳定、GC 后占用回落、业务命中率可接受 |
| 单个方法持续烧 CPU | TID、nid、JFR 样本同窗指向该方法 | 修复算法、输入边界或调用频率 | CPU、方法样本占比和接口 P99 同时下降 |
| 线程长期等待同一 owner | 多份栈的 owner/waiter 与业务超时一致 | 缩短临界区或修复资源归还 | owner 持有时间、等待线程和错误率下降 |
| HotSpot native 类别持续增长 | NMT baseline/diff 指向同一类别 | 修复对应类、线程或代码生命周期 | 相同阶段的 committed 增量不再单调上升 |
| 容器预算被非 heap 峰值突破 | OOMKilled、RSS、heap 与 NMT 时间线一致 | 收敛峰值或重算总预算 | 同流量下 RSS 保留安全余量且没有把压力转移到下游 |
复测时保持应用制品、JDK、JVM 参数、数据规模、请求模型、预热程度和采集窗口尽量一致。既看根因指标,也看业务 P95/P99、错误率、吞吐和下游压力;只让一个 JVM 指标变好、却把请求排队转移到数据库,并不算恢复。
只有重启后暂时恢复时,仍需保留未定位结论。为后续观察补齐持续 JFR、GC 日志、OOM dump 路径和版本记录,避免下次只能再做一次重启。
完成手动实验后,在启动探针的 Shell 中发出释放信号并等待退出:
touch "$LAB_OUT/release"
wait "$PID"JFR、线程栈和 HPROF 都可能包含业务数据。生产文件按受控留存流程处理;本地纯实验文件确认不再需要后,只清理本次目录:
case "$LAB_OUT" in
/tmp/jvm-diagnostics.*) rm -rf -- "$LAB_OUT" ;;
*) printf '拒绝清理未知路径\n' >&2 ;;
esac权威资料与规范地址
根据目标 JDK 查命令影响与事件定义;离线对象图和录制文件可结合 MAT、JMC 文档查阅。
