深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第四章知识点问答补充及重新排版
第四章 本文内容过多请根据目录查看
Q1: 列出 JDK 最常用的 5 个以上线上诊断工具,并分别用一句话说明主要用途。
- jcmd:统一诊断入口;线程、GC、堆转储、参数/属性、JFR 等一站式命令。
- jstat:采样查看 GC/类加载等运行时统计(轻量、低侵入)。
- jstack:导出线程快照,用于排查死锁、CPU 飙高、卡顿的热点栈。
- jmap:生成/查看堆转储、类直方图(线上使用需谨慎)。
- jinfo:查看/部分动态调整 JVM 运行参数与系统属性。
- jps:列出本机 Java 进程(PID、MainClass),便于定位目标进程。
- jhsdb:基于 SA 的离线剖析工具,进程挂死或无法附加时使用。
- JFR / JMC:低开销事件级性能录制与分析(飞行记录器 + 控制台)。
- (可选)JConsole / VisualVM:图形化监控与基本分析。
Q2: 列出你线上最常用的 jcmd 子命令(至少 6 个),并各用一句话说明在什么场景下使用。
- Thread.print:查看所有线程堆栈;排查高 CPU、死锁、卡顿等线程问题(可多次采样对齐 PID/栈帧)。
- GC.heap_info:快速查看堆与各代内存使用概况;判断是否老年代逼近阈值、Eden/Survivor 压力。
- GC.class_histogram:输出类直方图(实例数、占用);用于内存泄漏初筛/可疑大对象定位。
- GC.heap_dump filename=/path/heap.hprof:生成堆转储;用于精确泄漏分析(注意可能造成较长 STW)。
- VM.flags:查看 JVM 启动参数;核对GC 策略/内存大小/诊断开关是否按预期。
- VM.command_line:查看完整启动命令行;用于比对环境差异/容器参数透传问题。
- VM.system_properties:打印系统属性;定位时区/编码/代理等环境问题。
- VM.native_memory summary:查看 NMT 统计(需提前开
-XX:NativeMemoryTracking=summary|detail
);排查本地内存泄漏。 - JFR.start / JFR.dump / JFR.check:启动/导出/查看飞行记录;低开销线上性能录制(JDK 11+ 常用)。
- VM.uptime:进程运行时长;判断是否刚刚重启或问题出现前后窗口。
示例用法:
jcmd <pid> Thread.print
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_dump filename=/tmp/app.hprof
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summary
jcmd <pid> JFR.start name=prod settings=profile duration=120s filename=/tmp/rec.jfr
Q3: 解释 jstat -gcutil
各列的含义(如 S0/S1/E/O/M/CCS/YGC/YGCT/FGC/FGCT/GCT
),并给出如何通过这些指标快速判断“频繁 Young GC”“老年代压力过大”“可能内存泄漏”这三种常见情况的判读要点。
字段含义(jstat -gcutil
)
S0/S1
:Survivor0/1 区使用率(%)。E
:Eden 区使用率(%)。O
:Old/Tenured 老年代使用率(%)。M
:Metaspace 使用率(%,JDK8+)。CCS
:Compressed Class Space 使用率(%,JDK8+)。YGC
:Young GC 次数(进程启动以来累计)。YGCT
:Young GC 累计耗时(秒)。FGC
:Full GC 次数(累计)。FGCT
:Full GC 累计耗时(秒)。GCT
:总 GC 累计耗时(= YGCT + FGCT,秒)。
三种常见场景的快速判读
-
频繁 Young GC
- 现象:
YGC
在短时间内快速递增、YGCT
比例在GCT
中占比高;业务无峰值但 GC 次数高。 - 佐证:
E
经常接近 100% 且迅速清零(频繁回收);对象存活率高导致S0/S1
也偏高。 - 方向:加大新生代(如
-Xmn
/NewRatio
)、优化短命对象创建、检查热点分配点。
- 现象:
-
老年代压力过大
- 现象:
O
长期高位(>85%~90%)、FGC
增长、FGCT
在GCT
中占比升高。 - 佐证:
S0/S1
高 + 快速向老年代晋升(晋升失败风险)。 - 方向:排查大对象/长寿对象、降低晋升(如调
MaxTenuringThreshold
/Survivor 比)、增大老年代。
- 现象:
-
可能内存泄漏
- 现象:随着时间推移,
O
单调上升,即使发生FGC
也不回落;GCT
持续增加。 - 佐证:
M
/CCS
也缓慢爬升(疑似类加载/类加载器泄漏);GC.class_histogram
显示某些类实例数持续上涨。 - 方向:在稳定窗口抓堆(
jcmd GC.heap_dump
),比对多次直方图定位“只增不减”的可疑类型;排查缓存/静态集合/线程本地变量/类加载器链路。
- 现象:随着时间推移,
Q4: 线上第一时间快速体检 JVM,你会用哪几条命令(至少 3 条),各自看什么,如何根据输出做下一步动作?
-
CPU 高/线程异常路径
top -H -p <pid>
或ps -mp <pid> -o THREAD,tid,time
:找出占 CPU 最高的 TID。jcmd <pid> Thread.print
:按 **nid(十六进制)**定位对应线程栈(printf '0x%x\n' <tid>
做进制转换)。- 下一步:若大量
RUNNABLE
卡在某方法→查热点循环/IO;大量BLOCKED/WAITING
→查锁竞争/外部依赖;发现Found one Java-level deadlock
→按锁顺序修复。
-
内存/GC 路径
jstat -gcutil <pid> 1000 10
:连续 10 秒观察YGC/YGCT
、FGC/FGCT
、O
(老年代)是否异常增长。jcmd <pid> GC.heap_info
:看堆大小、各代占用与 GC 算法,判断是否逼近阈值。- (怀疑泄漏)
jcmd <pid> GC.class_histogram
:查看实例数/占用增长最快的类。 - 下一步:若老年代高位 + FGC 频繁→先扩容/限流/降级稳住,再在低峰执行
jcmd <pid> GC.heap_dump filename=/tmp/app.hprof
精准分析。
-
低开销全景记录(可选)
jcmd <pid> JFR.start name=hotfix settings=profile duration=120s filename=/tmp/rec.jfr
:两分钟事件级录制。- 下一步:用 JMC 打开
.jfr
看热点方法、分配热点、锁等待、IO/Socket 等。
线上首诊的优先级建议:
top/ps
→jcmd Thread.print
→jstat -gcutil
/jcmd GC.heap_info
→jcmd GC.class_histogram
→(必要时)heap_dump
。避免一上来就jmap
。
Q5: 线上 CPU 飙高时,如何将 top -H
里的高 CPU 线程映射到 jcmd Thread.print
的具体 Java 栈?请写出步骤与关键命令(含 TID→nid 十六进制 的转换),并说明你会在栈里重点关注哪些信号来判断“计算热点”“锁竞争”“IO 卡住”。
目标:把 top -H
/ps
里占 CPU 最高的线程(十进制 TID)映射到 jcmd Thread.print
的具体 Java 栈。
- 定位目标进程与高 CPU 线程(十进制 TID)
-
取 PID:
jps -l
(或ps aux | grep java
) -
看线程:
- Linux:
top -H -p <PID>
(或)ps -mp <PID> -o THREAD,tid,time,%cpu -r
- 记下 TID(十进制) 与 %CPU 最高的若干个
- Linux:
- 把十进制 TID 转成十六进制(映射 nid)
printf '0x%x\n' <TID>
# 例:TID=1234 -> 0x4d2
jcmd Thread.print
/jstack
中的线程标识写作nid=0x...
(十六进制)。
- 抓取并定位对应 Java 栈
jcmd <PID> Thread.print > /tmp/th.txt # 或:jstack -l <PID> > /tmp/th.txt
sed -n '/nid=0x4d2/,/^$/p' /tmp/th.txt # 打印从命中行到空行的该线程栈
- 读栈时的关键“信号”
- 计算热点:线程状态多为 RUNNABLE,栈顶在纯 Java 计算(业务方法、JSON/正则/流式聚合、BigDecimal/加密/压缩等),无明显锁/IO调用。
- 锁竞争:出现 BLOCKED / waiting to lock、
parking to wait for
,栈中可见synchronized
/ReentrantLock
/AbstractQueuedSynchronizer
等;多线程在同一锁处汇聚。 - IO 卡住:常见
java.net.SocketInputStream.socketRead0
、sun.nio.ch.*.read
、FileDispatcherImpl.read
等;NIO 选择器线程可见Selector.select
/EPollArrayWrapper.epollWait
(通常 RUNNABLE 但实际阻塞在 native)。
- 后续动作
- 计算热点 → 用 JFR 采样:
jcmd <PID> JFR.start settings=profile duration=120s filename=/tmp/rec.jfr
- 锁竞争 → 栈对齐冲突点,评估拆分锁/缩小临界区/无锁结构;必要时打印多次对比
- IO 卡住 → 排查下游依赖/网络/磁盘;对热点调用加超时与限流
Q6: 当你怀疑内存泄漏但不方便立刻做堆 Dump 时,如何用 jcmd GC.class_histogram
做“轻量级多次对比”,写出操作节奏(采样次数/间隔)、判读方法(哪些列看什么趋势)、以及何时升级为 GC.heap_dump
?
目标:在不能马上 dump 的情况下,用低侵入方法确认是否存在“持续增长”的可疑对象。
- 基线与配置核对(30–60s)
jcmd <pid> VM.flags
/VM.system_properties
(或jinfo -flags <pid>
):核对 GC、堆大小、Metaspace、是否开启 NMT 等。jstat -gcutil <pid> 5000 12
(每 5s,采 1 分钟):观察O
、YGC/YGCT
、FGC/FGCT
的增长速率与比例;GC.heap_info
快速看堆与代的占用。
- 轻量直方图多次对比(建议 3~5 次,间隔 20~30s)
jcmd <pid> GC.class_histogram > /tmp/histo1.txt
sleep 20
jcmd <pid> GC.class_histogram > /tmp/histo2.txt
sleep 20
jcmd <pid> GC.class_histogram > /tmp/histo3.txt
-
判读要点:
- 关注 Top-K 类(按
#bytes
降序),看#instances
与#bytes
是否跨样本单调上升。 - 若期间发生 Full GC(
FGC
递增),O
不明显回落(<5%~10%) 且 Top-K 类仍上升 → 泄漏嫌疑增强。 - 若
M
(Metaspace)/CCS
也在爬升,且直方图中类数/字节增长集中于某些动态生成/类加载相关类型 → 考虑 类加载器泄漏。 - Java 堆稳定但 RSS 持续涨 → 用
jcmd <pid> VM.native_memory summary
(需-XX:NativeMemoryTracking=summary|detail
)排查 本地内存。
- 关注 Top-K 类(按
- 何时升级为 Heap Dump(低峰执行)
-
满足以下任一:
O
持续上行,并且 1~2 次 FGC 后仍不回落到安全水位(常以 85%~90% 为危险线)。- 3+ 次直方图采样中,同一批 Top-K 类的
#bytes
/#instances
稳步增长(尤其在 FGC 之后仍增长)。 - Metaspace/CCS 接近上限或 NMT 显示特定类别增长异常。
-
执行:
jcmd <pid> GC.heap_dump filename=/tmp/app-$(date +%H%M).hprof
确认磁盘空间、写入路径、在业务低峰执行;Dump 后用 MAT/JMC 进行精确分析。
- 频率建议
jstat -gcutil
:1~5s 皆可(看场景与压力),持续 1~3 分钟更有趋势意义。GC.class_histogram
:≥20s/次(3~5 次即可);避免 1~3s 高频触发造成频繁 safepoint。- 如需更完整证据且低开销:用 JFR(JDK 11+)短录制:
jcmd <pid> JFR.start name=leak settings=profile duration=120s filename=/tmp/rec.jfr
查看 Allocation、Old Object(若可用)、Socket/IO、锁等待 等事件。
Q7: 如果你只能运行 一条命令 来快速判断“是 CPU 热点还是 GC/内存问题”,你会选哪条?请写出命令与理由,并说明“输出中你最先看的 3 个信号”。
jstat -gcutil <PID> 1000 20
理由:jstat -gcutil
能以极低开销提供时间序列 GC 指标,据此快速判断“是否 GC/内存瓶颈”。若 GC 迹象不强,基本可以把问题指向 CPU/锁/IO(再用其它手段深挖)。
最先看的 3 个信号(结合 20 次采样的趋势)
-
GCT
的斜率 ΔGCT/Δt:- ≈0(几乎不增长)→ 不是 GC 绑定;更可能是 CPU/锁/IO。
- 明显上升(例如 1s 内涨 0.2s 以上,且持续)→ GC 占用显著,偏向 GC/内存问题。
-
FGC
增长 &O
(Old)在 FGC 后是否明显回落**:FGC
递增但O
不回落或很少回落(<5%~10%)→ 老年代压力/潜在泄漏。- 几乎没有
FGC
,而O
保持低位→ 多半不是老年代问题。
-
YGC
增长速率 &E
(Eden)饱和-清空的频度:YGC
快速增长、E
频繁从高位清到低位→ 频繁 Young GC(短命对象多/新生代偏小)。YGC
平稳→ Young 区压力小。
若需要再加一条交叉验证(可选):
jcmd <PID> GC.heap_info
看各代占用是否逼近上限;但严格按“一条命令”要求时,仅用jstat
也能完成初判。
Q8: 只看一次 GC.heap_info
的静态快照容易误判。请写出一个“最小可行动态体检脚本”(3~5 行命令即可),每 2 秒采一次 jstat -gcutil
共采 10 次,并在每轮后顺便打一条 GC.heap_info
。写出命令或伪代码(shell 为佳),并说明你将如何用这些输出快速判断是否需要立刻 heap_dump
。
Shell(3~5 行,满足每 2 秒一次,共 10 次,并在每轮后输出 GC.heap_info
)
PID=<你的PID>
for i in {1..10}; dodate '+%F %T'jstat -gcutil $PID 2000 1 # 每2s取一次,仅取1个样本jcmd $PID GC.heap_info | egrep 'Min|Max|used|free|Eden|Survivor|Old|Metaspace|Compressed'
done
说明:
jstat -gcutil $PID 2000 1
自带 2s 间隔;循环 10 次即总计 ~20s。每轮后打印一次GC.heap_info
做轻量交叉验证。
如何用这些输出判断“是否立刻 heap_dump”
-
立即 dump 的触发(满足任一即可):
FGC
递增,但O
(Old)在 FGC 之后不回落或仅回落很少(<5%~10%),并且这一现象跨 3+ 轮连续出现;ΔGCT/Δt
明显(例如 2s 间隔内GCT
增长 ≥0.4s,且多轮持续),系统明显受 GC 影响;M
(Metaspace)/CCS
使用率高位(≥85%)且仍上行,存在类加载器相关风险;- 业务侧已出现 Allocation Failure、To-space exhausted 等 GC 日志告警(若日志可见)。
-
暂缓 dump,转 CPU/锁排查的信号:
GCT
基本不增长(ΔGCT/Δt≈0
),FGC
不动或极少;而系统仍慢 → 优先怀疑 CPU 热点/锁竞争/IO;此时再去top -H
+jcmd Thread.print
。
如需进一步确认而不想立刻 dump,可在稳定窗口低频(≥20–30s/次)追加 2–3 次
jcmd $PID GC.class_histogram
,关注 Top-K 类#bytes/#instances
是否在 FGC 后仍单调上升。
Q9: 已知某服务偶发 Stop-The-World 长暂停,你会如何用 jcmd VM.print_safepoint_statistics -verbose
与 JFR
组合定位暂停来源?请写出关键命令、你关注的 3 类字段/事件(如 TotalTime
/ApplicationTime
/TimeToSafepoint
、VMOperation
类型、GC
/ThreadDump
/ClassLoading
** 相关事件),以及各自的处置思路。
1) 看 Safepoint 统计(一次或多次)
jcmd <PID> VM.print_safepoint_statistics -verbose
关注 3 类关键信号:
-
总停顿与到点耗时
Total time for which application threads were stopped
(总停顿)Time to safepoint
(到达 safepoint 的同步时间)
判读:到点耗时高 ⇒ 有“坏公民”线程很久不进入安全点(长 JNI/大循环/编译缺少轮询点)。
-
VMOperation 类型与占比
表格中VM Operation
(如G1CollectForAllocation
、CollectForMetadataAllocation
、ThreadDump
、Enable/DisableBiasedLocking
、RedefineClasses
等)的 Count/Total(ms)/Mean(ms)。
判读:- 以
G1*Collect*
为主 ⇒ GC 引发的 STW,需看堆压力/晋升失败。 ThreadDump
多 ⇒ 外部频繁打栈造成停顿。RedefineClasses
多 ⇒ APM/热更/Agent 重定义导致。- 早期大量
BiasedLock*
⇒ 可考虑 禁用偏向锁(JDK8 可-XX:-UseBiasedLocking
,高版已移除)。
- 以
-
最大/平均停顿
Maximum time to safepoint
/Maximum VM operation time
判读:最大值尖峰可对齐业务告警时间,作为“问题窗口”。
2) 用 JFR 低开销录一段“证据带”
# 录制 2 分钟(JDK 11+)
jcmd <PID> JFR.start name=spf settings=profile duration=120s filename=/tmp/spf.jfr
# 或手动停止
# jcmd <PID> JFR.stop name=spf
在 JMC/JFR 里优先看 3 类事件:
GarbageCollection
/GC Pause
:看暂停时长、原因(Cause)、阶段(如 Evacuation/Remark/Cleanup)
→ 若与 safepoint 长停顿吻合,属 GC 路径。SafepointBegin/End
&VMOperation
:逐条事件定位 哪类 VM 操作导致停顿、到点耗时与 操作耗时 分别多大。- 线程/锁/IO 侧佐证:
JavaMonitorEnter
(锁等待)、ThreadPark
、SocketRead/Write
、FileRead/Write
→ 若到点耗时高且这些事件在热点线程上长时间 RUNNABLE/无 safepoint 轮询,考虑长 JNI/大计算循环。
3) 处置思路(对应不同来源)
-
到点耗时(TimeToSafepoint)偏高
- 排查长 JNI 调用/大循环无 safepoint 轮询(热点代码拆分、引入可中断点),避免长时间不检查轮询。
- 极端情况下可作为临时缓解:
-XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=...
(谨慎,根因仍需修)。
-
GC 引发的 STW
- 结合
jstat -gcutil
/GC.heap_info
看 老年代高位、FGC 频繁;必要时扩容/降级/限流,随后 heap dump 精确分析。 - 优化晋升/分配:调 Survivor 比例、
MaxTenuringThreshold
,减少大对象/长寿对象。
- 结合
-
Agent/重定义/频繁打栈
- 减少
ThreadDump
/RedefineClasses
触发频率;评估 APM/字节码增强对停顿的影响。
- 减少
-
偏向锁撤销尖峰(JDK8)
- 启动直接禁用偏向锁(
-XX:-UseBiasedLocking
)或延迟偏向;高版本已无偏向锁。
- 启动直接禁用偏向锁(
Q10: 进程假死/无法 jcmd
附加(但有 core 或可用 gdb
)时,你会怎样用 jhsdb
做离线剖析?请写出 3 步:
- 如何获取
core
或触发jmap
/gcore
; - 用
jhsdb jstack
/clhsdb
查看 Java 线程栈与死锁; - 用
jhsdb jmap
/jmap -histo
类似功能查看可疑类与内存分布。
前置注意:尽量使用与目标进程同版本同构建的 JDK 运行 jhsdb
(最好在同一台或同镜像内),否则可能因 libjvm.so
不匹配而报错。
① 获取 core(或在无法附加时生成)
-
方式 A(推荐,在线生成 core):
# 允许生成 core(若系统未开启) ulimit -c unlimited # 直接抓取 core(不杀进程) gcore -o /tmp/java-core <PID> # 生成 /tmp/java-core.<PID>
-
方式 B(系统统一 core 策略):
# 查看/设置 core 文件规则(需 root) cat /proc/sys/kernel/core_pattern echo '/var/coredumps/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern
-
方式 C(无法响应但仍在,可尝试“强制”堆转储作为佐证):
jmap -F -dump:format=b,file=/tmp/hang.hprof <PID> # 可能暂停较长,谨慎
kill -QUIT <PID>
只会把线程栈打到标准输出/日志,不会生成 core。
② 用 jhsdb 查看 Java 线程栈与(潜在)死锁
-
直接一条命令(离线 jstack):
jhsdb jstack --exe /path/to/java --core /tmp/java-core.<PID> > /tmp/jstack.txt
关注点:
nid=0x...
、STATE=RUNNABLE/BLOCKED/WAITING
、是否有
Found one Java-level deadlock
。- 热点线程是否卡在
java.net.Socket*
/sun.nio.*
(IO)、AbstractQueuedSynchronizer
(锁竞争)、业务热点方法(计算循环)。
-
交互式(控制台版 SA):
jhsdb clhsdb --exe /path/to/java --core /tmp/java-core.<PID> # 进入后常用命令: jstack -v threads printall
若还能本机附加(进程卡死但 JVMTI 不工作),也可:
jhsdb jstack --pid <PID>
。
③ 用 jhsdb 查看内存分布与可疑类型
-
类直方图 / 堆概览 / 类加载器统计:
jhsdb jmap --exe /path/to/java --core /tmp/java-core.<PID> --histo > /tmp/histo.txt jhsdb jmap --exe /path/to/java --core /tmp/java-core.<PID> --heap > /tmp/heap.txt jhsdb jmap --exe /path/to/java --core /tmp/java-core.<PID> --clstats > /tmp/clstats.txt
判读要点:
--histo
:按#bytes
/#instances
排序看 Top-K 类,是否为缓存/集合/字节数组等异常大户。--heap
:各代使用、GC 算法、老年代占用是否接近极限。--clstats
:类加载器分布——若某 Loader 负载异常且不可回收,警惕类加载器泄漏。
后续动作建议
-
线程侧:若大量
BLOCKED/parking to wait for
,锁竞争优化(缩小临界区、分段锁/无锁结构);若RUNNABLE
纯计算,优先 JFR 采样 + 算法/数据结构优化。 -
内存侧:Top-K 类型异常大 → 在低峰补抓在线
heap dump
精确分析;若 Metaspace 异常,检查动态生成类/Agent/热更。 -
证据补强:若恢复可附加,录制短 JFR:
jcmd <PID> JFR.start name=offline settings=profile duration=120s filename=/tmp/offline.jfr
Q11: 线上“老年代快速逼近 100% 并伴随 FGC 尖峰”的场景下,你会怎么临时止血(不重启)的 5 条手段?(如限流降级、扩大堆或老年代、调 Survivor 比例与晋升阈值、隔离大对象、关闭热点功能等),并写出各手段的适用前提与副作用。
-
限流/降级/熔断(网关或应用开关)
- 前提:有网关/开关可控;知道热点接口或租户。
- 副作用:部分请求失败/排队,SLA 下降;需回滚策略。
- 怎么做:对高分配率的接口/批量任务限速,关闭“导出/全量查询/大报表”等重操作。
-
压缩/清空内存型缓存(本地 Caffeine/Guava、热数据 Map、结果集缓存)
- 前提:缓存大小/TTL 可在线调整或可安全清空。
- 副作用:命中率下降、后端压力上升、短时延迟上涨。
- 怎么做:下调
maximumSize
/maximumWeight
,主动invalidateAll()
,禁止一次性“大 key 集合”缓存。
-
暂停或降速大批处理与后台任务(定时全量跑批、日志聚合、图片/报表生成)
- 前提:任务可停/可降速,或批量大小可调。
- 副作用:数据延迟、准实时性下降。
- 怎么做:减小 batch size/并发度;顺便排查是否存在结果集一次性装内存的代码路径。
-
降低日志/埋点/导出采样率,关闭大对象热点功能
- 前提:有动态配置;明确哪些功能产生大量临时对象(JSON/字符串拼接/压缩)。
- 副作用:可观测性变差;审计/排障线索减少。
- 怎么做:降采样比例、关 debug/trace、暂停“导出大 CSV/Excel”。
-
应急触发一次 Full GC(仅买时间)
- 前提:业务可承受一次较长 STW。
- 副作用:明显停顿;若真泄漏,回收效果有限。
- 怎么做:
jcmd <pid> GC.run
(或GC.run_finalization
);随后立刻做jstat -gcutil
连续观察 +GC.class_histogram
多次对比,决定是否 heap dump。
备选(若具备弹性能力):临时水平扩容/分流(加副本数、调度权重),以摊薄分配速率。副作用:资源成本、冷启动抖动。
Q12: 你要做**“堆外/本地内存”**的快速排查。请写出三步闭环:
- 如何判断“Java 堆稳定但 RSS 持续上涨”;
- 用
VM.native_memory summary
(NMT) 定位增长类目(需说明前置条件); - 若未开启 NMT 或证据不足,给出 2 条无侵入补救思路。
① 判断“Java 堆稳定但 RSS 持续上涨”
-
看堆与 GC:
jstat -gcutil <PID> 2000 10 # YGC/FGC/GCT 斜率是否小、O 区是否平稳 jcmd <PID> GC.heap_info # 堆各代 used/committed
结论要点:若多轮采样中 堆占用与 GCT 增速都很低,说明“Java 堆无明显增长”。
-
看进程 RSS:
cat /proc/<PID>/status | egrep 'VmRSS|VmSwap' cat /proc/<PID>/smaps_rollup | egrep 'Rss|Pss|Swap' pmap -x <PID> | sort -k3 -n | tail # 观察映射与已提交内存
若 RSS/Swap 持续上行,而堆不涨 ⇒ 高度怀疑 堆外/本地内存。
② 用 NMT(Native Memory Tracking)定位增长类目
前置条件:JVM 需以 NMT 启动:
-XX:NativeMemoryTracking=summary
(或detail
),否则VM.native_memory
无法使用;NMT 有轻微开销(一般 <2%)。
-
总览与对比:
jcmd <PID> VM.native_memory summary # 看各大类 reserved/committed jcmd <PID> VM.native_memory baseline # 设基线 # …等待一段时间… jcmd <PID> VM.native_memory summary.diff # 与基线对比增长
-
常见类别判读:
- Thread(reserved/committed):线程栈;线程数上涨或栈过大(
ulimit -s
)会推高。 - Class(Metaspace):类/元空间;类加载器泄漏或频繁热更会推高。
- Code:JIT 代码缓存;极端编译/代理增强导致增长。
- Internal / Other / Unknown:常见 DirectByteBuffer/Netty Arena/JNI 等堆外使用,重点关注。
- GC/Compiler:GC 内部/编译器分配,异常增长需结合版本/参数排查。
- Thread(reserved/committed):线程栈;线程数上涨或栈过大(
③ 未开 NMT 或证据不足时的无侵入补救思路(任选 2+)
-
对比内存映射(定位“谁在长”):
pmap -x <PID>
周期性采样并 diff;或smaps_rollup
观察 Rss/Pss/Swap 的时间序列变化,结合lsof -p <PID>
看是否因映射文件/共享库增长。 -
JFR 短录制(JDK 11+,低开销):
jcmd <PID> JFR.start name=nm settings=profile duration=120s filename=/tmp/nm.jfr
在 JMC 中查看 DirectBuffer/Socket/文件 IO/线程创建 等统计与热点;部分版本可见 Direct Memory 用量与分配热点。
-
JMX BufferPool 观测(直连或临时开启本地管理代理):
- 检查
java.nio:type=BufferPool,name=direct
的MemoryUsed/TotalCapacity/Count
是否持续上升; - 若未开本地代理,可尝试
jcmd <PID> ManagementAgent.start
(仅在安全环境使用)。
- 检查
-
线程数与栈大小核对:
ps -L -p <PID> | wc -l # 线程数 ulimit -s # 每线程栈大小
线程数×栈大小≈潜在 Thread committed,结合业务/池配置降并发或调小线程栈(谨慎)。
-
业务侧快速缓解:限流/降级、降低批处理并发、收紧本地缓存容量、减少一次性大对象(如大字节数组)、控制 Netty/Jemalloc Arena 等池化上限。
Q13: 线上发现 DirectByteBuffer 疑似泄漏:请写出你的确认与止血流程(3 步),包括
- 如何确认“直接内存”在涨(给出 2 种证据);
- 立即止血的 2 条手段(不重启);
- 中期修复的 2 个方向(参数/代码)。
① 如何确认“直接内存在涨”(给出 2+ 种证据)
-
JMX BufferPool(权威直观)
- 连接 JMX(必要时本机开启:
jcmd <pid> ManagementAgent.start_local
)。 - 查看
java.nio:type=BufferPool,name=direct
的MemoryUsed
/TotalCapacity
/Count
是否随时间单调上升(最好每 20–30s 采样 3–5 次)。
- 连接 JMX(必要时本机开启:
-
NMT(Native Memory Tracking)基线对比(需以
-XX:NativeMemoryTracking=summary|detail
启动)jcmd <pid> VM.native_memory baseline # 等 1–2 分钟 jcmd <pid> VM.native_memory summary.diff
- 关注
Internal
/Other
/Arena
/Thread
等类目中committed
的增长;DirectByteBuffer 常体现在Internal/Other
。
- 关注
-
RSS vs Java 堆(系统级交叉验证)
jstat -gcutil <pid> 2000 10 # 堆与 GCT 斜率很小 cat /proc/<pid>/status | egrep 'VmRSS|VmSwap'
- 若 Java 堆稳定而 RSS 持续上涨 → 高度怀疑堆外(直接内存/本地库)。
备注:
GC.class_histogram
看不到堆外内存,不要用它来“证明”Direct 泄漏。
② 立即止血(不重启)的 2 条手段
-
限流/降级,压制分配速率
- 对大流量/大报文接口限速、暂停大文件导出/批处理;减少并发连接/入站消息大小(若可动态配置)。
- 目的:让未被引用的 DirectBuffer 有机会被 GC 回收(Cleaner 触发)。
-
低风险补救动作
- 触发一次 GC(仅买时间):
jcmd <pid> GC.run
(对 已不可达 的 DirectBuffer 有效;若还被引用则无效)。 - 如果使用 Netty 且你有管理入口:调用
PooledByteBufAllocator.DEFAULT.trimCurrentThreadCache()
或你们封装的 allocator trim 方法,主动收缩线程本地缓存(版本相关、确保安全调用)。
- 触发一次 GC(仅买时间):
不能做的:运行期调大/调小
-XX:MaxDirectMemorySize
(需重启)。也不要高频GC.class_histogram
试图“看泄漏”。
③ 中期修复(参数/代码)各 2 条
参数/平台侧
-
设置上限:为直接内存加硬上限(需重启):
-XX:MaxDirectMemorySize=<大小>
;结合监控告警线。 -
JFR 证据链(JDK 11+):
jcmd <pid> JFR.start name=dm settings=profile duration=120s filename=/tmp/dm.jfr
在 JMC 里看 DirectBuffer/分配热点/大对象来源;必要时在灰度环境开启 Netty ResourceLeakDetector(
ADVANCED/PARANOID
)定位未释放路径。
代码/业务侧
- 确保释放:Netty/ByteBuf 路径使用
try { ... } finally { buf.release(); }
;避免 slice/duplicate 后忘记释放原引用。 - 降内存形态:大对象优先用 堆内缓冲 或 分块/流式处理(避免一次性
allocateDirect
大片内存);限制聚合器/反序列化的最大消息长度。
Q14: 只使用 JFR/JMC 工具链,在线上低开销地捕捉一次 2–3 分钟的证据以定位“CPU 热点 + 内存分配热点”。请写出:
- 你会使用的 完整命令序列(
JFR.start
/JFR.check
/JFR.dump
/JFR.stop
,含settings
、duration
、filename
等关键参数); - 导出后的
.jfr
在 JMC 里你优先查看的 5 个视图/事件; - 依据这些视图你会做的 两条处置动作(分别针对“计算热点”和“分配过猛”)。
1) 命令序列(JDK 11+)
# ① 启动录制(2~3 分钟),低开销配置,进程退出也会自动落盘
jcmd <PID> JFR.start name=triage settings=profile duration=180s dumponexit=true filename=/tmp/triage.jfr# ② 中途确认状态/进度
jcmd <PID> JFR.check# ③ 如需随时导出一份快照(不中断录制)
jcmd <PID> JFR.dump name=triage filename=/tmp/triage-snapshot.jfr# ④ 如需手动提前结束
jcmd <PID> JFR.stop name=triage
说明:
settings=profile
通常用于线上短时排查;如需更低开销可用default
,但采样粒度更粗。
2) 在 JMC 里优先看的 5 个视图/事件
- Method profiling / Hot methods(CPU 热点方法、调用栈占比)
- Allocation (TLAB / Outside TLAB)(分配速率与分配热点类型/方法)
- Garbage Collection(GC 暂停、原因、阶段耗时)
- Locks / Java Monitor Blocked(锁竞争、阻塞时间、热点锁)
- I/O(Socket/File)(大量读写或慢 I/O 是否与尖峰同窗)
3) 两条处置动作
-
针对“计算热点(CPU)”:
- 定位前 1–2 个热点方法 → 优化算法/数据结构、下移/缓存结果、拆小批;若是自旋/忙等 → 加退避/超时。
-
针对“分配过猛(内存)”:
- 对最大分配源做对象重用/缓冲池/流式处理,削峰(限流/降级);必要时扩大新生代或 TLAB(重启后生效),先稳住 GC 压力。
Q15: 线上服务偶发 高 CPU + 频繁 Young GC。请写出你的三段式排查策略:
- 快速体检:给出 3 条命令看什么(含节奏);
- 定位根因:分别从“计算热点”“短命对象暴增”两条支线给出各 2 个证据;
- 缓解到修复:各给 2 条(不重启的缓解 / 重启后的修复)。
1) 快速体检(3 条命令 + 节奏 + 看点)
-
top -H -p <PID>
(或ps -mp <PID> -o THREAD,tid,%cpu -r
),连看 10–20s- 看 最高 CPU 的 TID 列表(十进制)。
-
jstat -gcutil <PID> 1000 20
(每 1s × 20 次)- 看 ΔGCT/Δt、
YGC
增速、E
/S0/S1
的“充满→清空”频度、O
是否稳定。
- 看 ΔGCT/Δt、
-
jcmd <PID> Thread.print > /tmp/th1.txt ; sleep 2 ; jcmd <PID> Thread.print > /tmp/th2.txt
- 用 TID→nid(0x…) 映射定位热点线程栈(计算/锁/IO)。
2) 定位根因(两条支线,各给 2 个证据)
-
A. 计算热点(CPU)
- 证据1:热点线程 RUNNABLE,栈顶在 纯 Java 计算(JSON、正则、加密、流式聚合等),多次采样栈帧稳定。
- 证据2:
ΔGCT/Δt ≈ 0
或很小,但系统仍慢 → 非 GC 瓶颈;CPU 使用率/单核饱和明显。
-
B. 短命对象暴增(频繁 Young GC)
- 证据1:
YGC
快速递增、E
频繁从高位清到低位,YGCT
斜率明显。 - 证据2:
jcmd <PID> GC.class_histogram
低频(≥20s/次)多次对比,Top-K 类型#instances/#bytes
同窗口持续上升;或用 JFR Allocation 看到分配热点方法/类型。
- 证据1:
3) 缓解 → 修复
-
不重启的缓解(任选 2)
- 限流/降级 高分配率接口,暂停大报表/批处理,降低并发;缩小本地缓存容量/TTL。
- 临时证据采集:开启 JFR 2–3 分钟(
settings=profile
),定位热点;必要时 一次性 GC(jcmd GC.run
)仅用于买时间。
-
重启后的修复(任选 2)
- GC/内存参数:合理设定新生代比例(如 G1 的
G1NewSizePercent/G1MaxNewSizePercent
)、MaxTenuringThreshold
、TLAB
(结合压测验证)。 - 代码层:减少临时对象(复用缓冲、对象池、流式处理)、优化热点算法与数据结构;限制序列化/聚合的最大报文/批量。
- GC/内存参数:合理设定新生代比例(如 G1 的
Q16: 当你已确认“频繁 Young GC”且 Old
基本稳定时,请写出一份专治短命对象暴增的 checklist(至少 8 条,覆盖代码与配置两侧),并按“定位 → 改善 → 复验”的顺序编排。
一、定位(低侵入取证)
- 时间序列 GC 体检:
jstat -gcutil <PID> 1000 60
连续 60 秒;看ΔGCT/Δt
、YGC
增速、E/S0/S1
是否“频繁充满→清空”。 - 堆概览:
jcmd <PID> GC.heap_info
快速确认各代占用与阈值(验证 Old 基本稳定、Young 压力大)。 - 分配热点(首选):
jcmd <PID> JFR.start name=alloc settings=profile duration=120s filename=/tmp/alloc.jfr
;在 JMC 里看 Allocation (TLAB/Outside TLAB) 的类型/方法热点与分配速率。 - 类直方图(低频对比):
jcmd <PID> GC.class_histogram
每 20–30s 采 3–5 次,对比 Top-K 类型#instances/#bytes
趋势(注意频率别过高以免频繁 safepoint)。 - 业务归因:对齐网关/QPS/接口耗时时序,定位哪个接口/任务触发突增(如大报表、全量导出、批处理)。
- 启动参数核对:
jcmd <PID> VM.flags
/VM.command_line
确认 GC 策略、Young 大小(G1 的G1NewSizePercent/G1MaxNewSizePercent
)与MaxTenuringThreshold
等是否异常。 - 日志快速佐证:若可用,查看 GC 日志中 Allocation Failure 频率及 Young GC 原因。
- 排除堆外干扰:
jcmd <PID> VM.native_memory summary
(若启用 NMT)或对比 RSS vs 堆(/proc/<pid>/status
+jstat
),确认问题确是堆内短命对象而非堆外。
二、改善(先缓解再修复)
代码层(优先)
- 减少临时对象:复用
StringBuilder
/byte[]
/缓冲区;避免链式map/filter
产生大量中间对象。 - 集合预尺寸:对
ArrayList/HashMap
等预估容量,避免扩容与 rehash 抖动。 - 避免装箱/拆箱:使用原始类型与
TIntArrayList
等(如可用第三方),减少Integer/Long
短命对象。 - JSON/XML 流式化:改用流式解析/序列化,复用
ObjectMapper
/Gson
等重对象,避免频繁 new。 - 分页/分块:长链路/大结果集改为分页或流式处理,禁止一次性
readAll()
、collect(toList())
装全量。 - 缓存策略:对重复密集计算结果短期缓存(权衡内存),但避免把大对象缓存成“长命”。
- 日志与埋点控量:降低高频路径上的字符串拼接/序列化开销(降采样或延迟拼接)。
- ThreadLocal 谨慎复用:可复用重对象但注意清理,避免跨请求遗留。
配置/参数(多需重启)
9. 增大新生代:G1 场景调 G1NewSizePercent/G1MaxNewSizePercent
(或其他收集器的 -Xmn
)以降低 Young GC 频率。
10. 晋升阈值:适度提高 MaxTenuringThreshold
,减少早晋升(结合 Survivor 比例);避免老年代被短命对象顶满。
11. TLAB 调优:让热点线程有足够 TLAB(通常交给 JVM 自适应;如需,压测后再调)。
12. 运行期缓解(无需重启):对高分配率接口限流/降级,暂停“导出/全量报表/大批处理”。
三、复验(对比前后效果)
jstat -gcutil
再观测 1–3 分钟:YGC 每分钟显著下降;YGCT/GCT
占比下降。- JFR 复录 2 分钟:Allocation 视图中Top 类型/方法的分配速率下降,Outside TLAB 尖峰减少。
- 类直方图对比:Top-K 类型的
#instances/#bytes
不再单调上升。 - 业务 SLO:P95/P99 延迟与吞吐恢复到阈值内;无 Young GC 风暴告警。
Q17(GC 日志 / 统一日志) 请回答以下三小问(要点式即可):
- JDK 8 如何开启最小但有用的 GC 日志?给出常用启动参数各自作用。
- JDK 11+ 如何在线上临时开启/导出 GC 统一日志(
-Xlog
),以及常用的jcmd VM.log
用法各一条? - 拿到 GC 日志后,你首看哪 3 个信号来判断“是否 GC 绑定”(例如暂停时长、原因、频度/斜率等)?
1) JDK 8 ——最小但有用的 GC 日志启动参数
-XX:+PrintGCDetails
:输出 GC 详细信息(代、原因、回收前后用量)。-XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
:带上日期与进程启动相对时间。-Xloggc:/var/log/app/gc.log
:落盘到文件。-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
:滚动日志,避免单文件过大。- (可选)
-XX:+PrintTenuringDistribution
:观测对象年龄分布与晋升阈值效果。
精简范例:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/var/log/app/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
2) JDK 11+ ——在线临时开启 / 导出统一日志(-Xlog
)与 jcmd VM.log
- 启动就开(滚动+装饰器):
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
- 运行期用
jcmd VM.log
动态开启到滚动文件:
jcmd <pid> VM.log what=gc* safepoint=info decorators=uptime,level,tags filename=/var/log/app/gc.log filecount=5 filesize=20M
- 运行期直接打到 stdout(便于快速取证):
jcmd <pid> VM.log output=stdout what=gc+heap=info decorators=uptime,level,tags
- 停止:
jcmd <pid> VM.log disable
说明:统一日志选择器形如
gc*
、gc+heap
、safepoint
;decorators
常用time|uptime|level|tags
。
3) 拿到 GC 日志后,首看 3 个信号判断是否“GC 绑定”
- 暂停时长与占比:Young/Full 的单次暂停是否长、短时间内总 GC 时间占比是否高(例如过去 1–5 分钟内 GCT 占比异常)。
- 频度与斜率:GC 事件是否越来趟越密(相邻两次间隔缩短、单位时间次数上升)。
- 回收后占用趋势:老年代/堆在 GC 之后是否明显回落;若 FGC 后仍不降或持续上行,偏向老年代压力/泄漏。
Q18(JIT / Code Cache 与容器注意项) 要点式回答:
- 如何判断 Code Cache(JIT 代码缓存)逼近上限?(给出 2 条证据与 1 条止血手段)
- 容器(K8s)里排查 JVM 问题时,相比裸机需要额外注意的 3 点(如 cgroup 内存/CPU 限制、时区/时钟、
pid
/nsenter
等取证差异)。
1) 判断 Code Cache 逼近上限(2 证据 + 1 止血)
-
证据A|统一日志(JDK 11+)
- 临时打开:
jcmd <pid> VM.log what=codecache=info
(或启动加-Xlog:codecache=info
)。 - 关注日志中的 “CodeCache is full / sweeper”、各段(non-profiled/profiled/non-nmethods)使用率。
- 临时打开:
-
证据B|NMT 统计(需
-XX:NativeMemoryTracking=summary|detail
启动)jcmd <pid> VM.native_memory summary
,看 Code 类目的 committed 是否持续逼近 ReservedCodeCacheSize。- 也可核对
jcmd <pid> VM.flags
里的ReservedCodeCacheSize
。
-
止血手段(在线)
- 减少新编译产出:用编译指令排除易爆热点方法
jcmd <pid> Compiler.directives_add '[{"match":"com.xxx.Hot*","Exclude":true}]'
然后Compiler.directives_print
验证;这能立刻减缓 Code Cache 增长。 - (根治在重启时扩大
-XX:ReservedCodeCacheSize
或优化热点;确认-XX:+UseCodeCacheFlushing
开启)
- 减少新编译产出:用编译指令排除易爆热点方法
2) K8s/容器环境排查的 3 个额外注意点
-
cgroup 资源与 JVM 配置
- 旧 JDK 需显式启用容器感知:
-XX:+UseContainerSupport
(8u191+);建议用百分比:-XX:MaxRAMPercentage=… -XX:InitialRAMPercentage=…
,并给 堆外(Metaspace/Direct/ThreadStack/CodeCache)留冗余,避免 OOMKill。 - CPU 配额会影响并行度与停顿,必要时设
-XX:ActiveProcessorCount=<quota核数>
。
- 旧 JDK 需显式启用容器感知:
-
取证与命名空间
- 容器内的 PID 与主机 PID 不同;使用
kubectl exec
/docker exec
在容器内跑jcmd/jstat
;需要查看主机/proc
时用nsenter -t <hostPid> -m -u -i -n -p
。 core dump
常被禁用,提前设ulimit -c unlimited
或用gcore <pid>
生成 core 到可写目录。
- 容器内的 PID 与主机 PID 不同;使用
-
时区/时钟与日志路径
- 统一时区:
-Duser.timezone=Asia/Shanghai
(或镜像内 TZ);保证 GC/JFR/业务日志时间对齐,便于关联分析。 - 日志卷挂载与文件轮转在容器内路径保持一致(GC 统一日志
-Xlog:...:filecount,filesize
)。
- 统一时区:
Q19(hs_err_pid
崩溃日志快速判读)
请要点式回答以下三小问:
-
拿到
hs_err_pid*.log
,你第一时间会看哪 6 个关键字段/段落,并各用一句话说明它能帮你判断什么?(例如:# A fatal error has been detected by the Java Runtime Environment
,EXCEPTION_ACCESS_VIOLATION/Signal
,Problematic frame
,Thread
、Current CompileTask
、GC
段、VM Arguments
、OS/CPU
、loaded libraries
、Native frames
、Java frames
、Heap
摘要等) -
如何初步归因是 JVM Bug / 第三方本地库(JNI) / 代码缓存(Code Cache) / 栈溢出 / 元空间(Metaspace) / 直接内存(Direct) 等?请各给1 条判据(来自日志中的具体线索,如
Problematic frame
指向某个.so
、Stack: [low, high] guard pages
、CodeCache: full
、Metaspace
统计、direct buffer
提示等)。 -
当场止血的 2 条动作(不重启优先,若必须重启请写出启动参数级的临时止血项各 1 条),并各说明副作用。
1) 拿到 hs_err_pid*.log
,先看这 6 个关键段落
-
错误头部与信号
A fatal error has been detected…
、EXCEPTION_ACCESS_VIOLATION
/SIGSEGV
/SIGBUS
、at pc=…
、JRE version/VM
- 用途:确定崩溃类型、PC 地址、JDK 版本与构建(是否命中过已知 Bug)。
-
Problematic frame
- 显示出错的库与符号(如
libjvm.so
、libnetty_transport_native_epoll.so
、libc.so
)。 - 用途:快速归因 JVM 本身 vs 第三方 JNI/系统库。
- 显示出错的库与符号(如
-
Current thread / JavaThread(含
Stack: [low, high]
)- 线程名/状态、是否在编译/GC/VM 线程;栈范围与保护页。
- 用途:判断是业务线程还是 VM 内部线程;栈范围可辅助识别栈溢出。
-
Native frames / Java frames 回溯
- C/C++ 调用栈 + Java 调用栈(如
Native method
、业务方法)。 - 用途:定位最后的代码路径与是否由某 JNI 调用触发。
- C/C++ 调用栈 + Java 调用栈(如
-
VM Arguments
- 启动参数(GC、
ReservedCodeCacheSize
、MaxMetaspaceSize
、MaxDirectMemorySize
等)。 - 用途:核对是否存在过小上限/危险开关、便于给临时止血参数。
- 启动参数(GC、
-
内存与代码缓存摘要
Heap
/Metaspace
概要、CodeCache:
各段使用率与是否full
。- 用途:识别 内存类问题(Metaspace/堆)与 JIT 代码缓存压力。
备查:
OS/CPU
、loaded libraries
、Current CompileTask
、GC
片段也很有用(平台差异、编译中崩溃、GC 上下文)。
2) 初步归因的判据(各给一条来自日志的线索)
- JVM Bug:头部出现
Internal Error (xxx.cpp:line), guarantee()/assert failed
,且 Problematic frame 指向libjvm.so
。 - 第三方 JNI 库:Problematic frame 指向某
.so/.dll
(如libfoo.so
),Java 栈顶为Native method
。 - Code Cache(JIT):
CodeCache: … used=… free=…
接近上限或日志提示CodeCache is full
;Current CompileTask
活跃并指向 C2。 - 栈溢出:
Stack: [low, high]
很小/已贴近保护页;常见SIGSEGV
伴随stack guard pages
描述或之前出现StackOverflowError
。 - Metaspace 压力/枯竭:
Metaspace
used≈committed≈MaxMetaspaceSize
;崩溃前常伴随Metadata GC Threshold
;(若触发 OOME 也可能在日志/应用日志中可见)。 - 直接内存(Direct):VM 参数
-XX:MaxDirectMemorySize
很小且业务大量 NIO/Netty;Java 栈/日志中出现OutOfMemoryError: Direct buffer memory
;Problematic frame 在libc malloc/mmap
也可佐证堆外分配失败。
3) 当场止血(不重启优先;必须重启的写启动参数)+ 副作用
不重启优先
-
流量/功能止血:对可疑接口/任务限流降级、关闭类重定义/热更/频繁打栈(APM/调试)路径。
- 副作用:SLA 降低/可观测性变差,但能快速降低触发概率。
-
JIT/CodeCache 缓压:
jcmd <pid> Compiler.directives_add '[{"match":"com.xxx.hot.*","Exclude":true}]' jcmd <pid> Compiler.directives_print
- 副作用:被排除方法降级为解释或低级别编译 → 吞吐下降/延迟上升,但可避免 CodeCache 继续被写满或命中编译器崩溃路径。
-
堆外/直接内存:限流 +(Netty)trim 线程缓存、减小单请求消息/批量大小;必要时触发一次
jcmd <pid> GC.run
仅回收已不可达的 DirectBuffer。- 副作用:吞吐降低;
GC.run
可能带来可见 STW。
- 副作用:吞吐降低;
-
停止外部高频
jstack
/kill -3
/ThreadDump
行为(若VM.print_safepoint_statistics
显示大量ThreadDump
操作)。- 副作用:排障证据减少。
需要重启的临时参数
- Code Cache:
-XX:ReservedCodeCacheSize=<更大>
并确保-XX:+UseCodeCacheFlushing
;
副作用:进程常驻内存增加,冷启动时编译更久。 - 栈溢出:
-Xss=<更大>
或降低线程并发;
副作用:每线程内存更大/并发度下降。 - Metaspace:
-XX:MaxMetaspaceSize=<更大>
,减少热更/动态生成类;
副作用:RSS 增长。 - 直接内存:
-XX:MaxDirectMemorySize=<合理上限>
;
副作用:不当增大可能掩盖泄漏并推高 RSS。 - 编译器绕行(疑似 JIT 崩溃):
-XX:-TieredCompilation
或-XX:-UseC2
(退回 C1),极端可-Xint
;
副作用:性能显著下降(逐级加大)。
Q20(OOM 取证与快速处置)围绕 OutOfMemoryError,完成以下要点(要点式即可):
-
启动期的取证参数清单(JDK8 vs JDK11+)
- 至少写出 6 条常用参数,并各用一句话说明作用(例如:
-XX:+HeapDumpOnOutOfMemoryError
、-XX:HeapDumpPath=...
、-XX:+ExitOnOutOfMemoryError
、-XX:OnOutOfMemoryError="..."
、-XX:StartFlightRecording=...
/-XX:FlightRecorderOptions=...
、统一 GC 日志-Xlog:gc*,safepoint:...
或 JDK8 的-Xloggc
+ 滚动等)。
- 至少写出 6 条常用参数,并各用一句话说明作用(例如:
-
事发后 60 秒内的动作序列(不重启)
- 写出 至少 6 步,包含具体命令/节奏与目的(例如:
jstat -gcutil <pid> 1000 20
连续 20 秒、jcmd <pid> GC.heap_info
、jcmd <pid> GC.class_histogram
低频 1–2 次、jcmd <pid> JFR.start ... duration=120s
、top -H
+jcmd Thread.print
、网关限流/关闭导出等),并注明哪些动作可能带来 STW、哪些是低侵入。
- 写出 至少 6 步,包含具体命令/节奏与目的(例如:
-
按 OOM 类型给出止血与修复(各 2 条)
-
以表格或要点列出 至少 4 类:
Java heap space
/GC overhead limit exceeded
Metaspace
Direct buffer memory
unable to create new native thread
(或CodeCache full
任选其一也可加上)
-
每类分别给出 不重启的快速止血 2 条 与 重启后的修复 2 条,并简述副作用(例如:限流/降级、一次性
GC.run
的停顿风险、扩大-Xmx
/MaxMetaspaceSize
/MaxDirectMemorySize
/ReservedCodeCacheSize
的 RSS 影响、调小-Xss
的栈空间影响等)。
-
1) 启动期取证参数清单(JDK8 / JDK11+)
-
通用(8 与 11+ 都适用)
-XX:+HeapDumpOnOutOfMemoryError
:发生 OOM 自动生成堆转储(hprof)。-XX:HeapDumpPath=/var/log/app/heapdump.hprof
:指定 dump 路径/目录。-XX:+ExitOnOutOfMemoryError
:出现 OOM 直接退出(避免“半死不活”占资源)。-XX:OnOutOfMemoryError="sh /app/bin/oom-hook.sh %p"
:OOM 时执行自定义钩子(打包日志、上报等)。-XX:ErrorFile=/var/log/app/hs_err_pid%p.log
:收集 HotSpot 崩溃日志。
-
JDK 8(GC 日志与 JFR)
-Xloggc:/var/log/app/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
:GC 详细日志。-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
:GC 日志滚动。- (若可用)
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=name=boot,duration=5m,filename=/var/log/app/boot.jfr
:启动即录制一段 JFR(Oracle JDK8 等环境)。
-
JDK 11+(统一日志与 JFR)
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
:统一 GC/安全点日志(滚动)。-XX:StartFlightRecording=name=boot,settings=profile,dumponexit=true,filename=/var/log/app/boot.jfr
:启动自动录 JFR。-XX:FlightRecorderOptions=stackdepth=128,samplethreads=true
:提高可观测性(按需)。
2) 事发后 60 秒内的动作序列(不重启)
目标:留证据、压住风险、初步归因。标注:🟢低侵入 🟡可能短暂停顿(进入安全点) 🔴明显 STW 风险
- 打开统一日志(若未开) 🟢
jcmd <pid> VM.log what=gc*,safepoint decorators=uptime,level,tags filename=/var/log/app/gc.live.log filecount=5 filesize=20M
- 连续 GC 体检(20 秒) 🟢
jstat -gcutil <pid> 1000 20
→ 看ΔGCT/Δt
、YGC/FGC
斜率、O
是否回落。 - 堆概览 🟢
jcmd <pid> GC.heap_info
→ 各代 used/committed、算法与阈值。 - 类直方图(低频 1–2 次,间隔 20–30s) 🟡
jcmd <pid> GC.class_histogram > /tmp/histo1.txt
;sleep 20
;再采一次对比 Top-K#bytes/#instances
趋势。 - 启动 JFR 取证(120 秒) 🟢
jcmd <pid> JFR.start name=oomtriage settings=profile duration=120s filename=/tmp/oomtriage.jfr
→ 捕捉分配热点/锁/IO。 - CPU/线程侧交叉验证(必要时) 🟡
top -H -p <pid>
→ 取 TID;jcmd <pid> Thread.print > /tmp/th.txt
→ 排除计算/锁导致的内存放大。 - 业务侧止血(立刻执行) 🟢
限流/降级/暂停导出与批处理、缩小分页;下调本地缓存上限;避免一次性加载全量数据。 - (极端)买时间动作 🔴
jcmd <pid> GC.run
(一次性 GC);或在低峰jcmd <pid> GC.heap_dump filename=/tmp/oom-$(date +%H%M).hprof
(大概率 STW,谨慎)。
3) 分类型止血与修复(各 2 条,含副作用)
A. Java heap space
/ GC overhead limit exceeded
-
不重启止血
- 限流/降级、暂停大任务与导出;缩小查询批量、分页返回。副作用:SLA 降低、时延可能上升。
- 降低本地缓存容量/TTL,清理热点 Map/集合。副作用:缓存命中率下降,后端压力上升。
-
重启后修复
- 增大堆(
-Xmx
,配合容器百分比参数);合理放大新生代(G1 的G1NewSizePercent/G1MaxNewSizePercent
)。副作用:RSS 增、回收停顿形态变化。 - 代码侧减分配:对象复用/流式处理、集合预尺寸、避免装箱/链式中间对象,限制最大报文与结果集。副作用:实现复杂度上升
- 增大堆(
B. Metaspace
-
不重启止血
- 停止热更/字节码重定义/频繁生成代理类;回收无需的插件/模块。副作用:功能受限、可观测性下降。
- 限流相关特性入口(若是动态脚本/模板导致类爆增)。副作用:业务能力下降
-
重启后修复
- 增大
-XX:MaxMetaspaceSize
并监控;优化类加载器生命周期、修复 ClassLoader 泄漏(确保close()
/释放引用)。副作用:RSS 增 - 减少动态类生成(代理/ASM/CGLIB),复用单例对象映射。副作用:灵活性下降
- 增大
C. Direct buffer memory
-
不重启止血
- 限流/降并发、降低单次消息/文件大小;(Netty)
trim
线程缓存或关闭过度池化;触发一次GC.run
仅回收已不可达的 DirectBuffer。副作用:吞吐下降;GC 有可见停顿 - 避免将 DirectBuffer 放入长寿命缓存/队列;尽快释放引用(Netty
ByteBuf.release()
)。副作用:需要变更调用方逻辑
- 限流/降并发、降低单次消息/文件大小;(Netty)
-
重启后修复
- 合理设
-XX:MaxDirectMemorySize
上限并监控 BufferPool(JMX)。副作用:上限过小会更早 OOM,过大推高 RSS - 代码侧改为堆内缓冲或分块/流式,严格
try/finally
释放。副作用:可能牺牲少量性能
- 合理设
D. unable to create new native thread
-
不重启止血
- 动态下调线程池最大线程数/阻塞队列上限,熔断低价值请求;排查线程泄漏。副作用:吞吐降低、排队增多
- 检查/临时提升 OS
ulimit -u
(若策略允许);杀掉异常子进程。副作用:系统级策略变更需审慎
-
重启后修复
- 降低
-Xss
(单线程栈更小,腾出创建空间);统一线程池与复用模型,避免 per-request 新线程。副作用:栈太小可能触发 StackOverflow - 架构上改为异步/事件驱动,减少线程数量级。副作用:改造成本
- 降低
(可选)E. CodeCache 满导致崩溃/异常
- 不重启止血:
jcmd <pid> Compiler.directives_add '[{"match":"com.xxx.hot.*","Exclude":true}]'
抑制编译产物增长。副作用:热点降级为解释或低级别编译,性能下降 - 重启后修复:增大
-XX:ReservedCodeCacheSize
、确认-XX:+UseCodeCacheFlushing
开启,或减少极端内联/编译热度。副作用:RSS 增、冷启动时编译更久
Q21(GC 选择与参数基线:G1 vs ZGC)
-
场景选型:在什么情况下优先选择 G1,什么情况下优先选择 ZGC?请给出至少 6 条决策因素(如堆大小/暂停目标/吞吐 vs 延迟/JDK 版本与成熟度/本地内存占用/容器化/对象寿命分布/可观测性与调参复杂度等),并分别简述理由。
-
G1 基线:给出一套你在生产容器环境常用的 G1 启动参数基线(写成一行或多行均可),并逐项用短语说明作用(例如堆与百分比:
-Xms/-Xmx
或-XX:InitialRAMPercentage/MaxRAMPercentage
、暂停目标-XX:MaxGCPauseMillis
、新生代上限下限G1NewSizePercent/G1MaxNewSizePercent
、触发并发标记InitiatingHeapOccupancyPercent
、保留比例G1ReservePercent
、并行/并发线程、AlwaysPreTouch
、统一 GC 日志滚动等)。 -
ZGC 基线:给出一套 ZGC 启动参数基线(JDK 17+ 场景优先),同样逐项用短语说明作用(如
-XX:+UseZGC
、SoftMaxHeapSize
、ZUncommitDelay
、ZCollectionInterval
、ConcGCThreads
、统一日志、容器内存百分比参数等),并说明 ZGC 无显式暂停目标时你如何验收上线效果(列出 3 个你观察的指标/阈值,如 STW P99、Allocation Rate、突增回收时长等)。
1) 场景选型:什么时候选 G1,什么时候选 ZGC(至少 6 点)
-
堆大小
- G1:中等堆(~4–32/64 GB)常见、成熟。
- ZGC:超大堆(几十 GB~TB 级)更稳,并发压缩基本不随堆变慢。
-
暂停目标 / 业务诉求
- G1:可用
MaxGCPauseMillis
定目标,常见 100–200 ms 等级。 - ZGC:追求极低延迟(常见 <10–20 ms P99),对抖动更敏感的业务优先。
- G1:可用
-
吞吐 vs 延迟权衡
- G1:总体吞吐通常更好(更少并发搬迁开销)。
- ZGC:为低延迟付出一点吞吐与 CPU 并发开销。
-
JDK 版本与成熟度
- G1:JDK8/11 起长期打磨、参数/实践多。
- ZGC:JDK17+ 非常成熟(生产可用),JDK21+ 支持代际 ZGC(更优分代分配/回收)。
-
碎片与压实能力
- G1:分区+按需混合回收,停顿中压实;碎片控制尚可。
- ZGC:并发移动对象,碎片控制最佳,长时间运行更平稳。
-
内存弹性 / 释放空闲
- G1:可回收 free region,但对RSS 回落不如 ZGC 灵活。
- ZGC:
SoftMaxHeapSize
+ uncommit 机制,闲时能主动把内存还给 OS。
-
容器与资源约束
- G1:更易控的资源曲线;参数多、可细调适配小配额。
- ZGC:对 CPU 并发线程有一定要求,极小 vCPU/内存配额时需评估 GC 线程竞争。
-
调参复杂度 / 可观测性
- G1:可调旋钮多(新生代比例/触发阈值/保留比例等),可精细化治理。
- ZGC:几乎零调参即可达标,靠
-Xlog
/JFR 看效果、改容量即可。
简单判定:低延迟/大堆/长时稳定→ ZGC;普通电商/报表、中等堆、强调吞吐与成本→ G1。
2) 生产容器环境 G1 启动参数基线(示例)
# 容器感知 + 堆比例(JDK11+ 默认容器感知;8u191+可显式打开)
-XX:+UseContainerSupport
-XX:InitialRAMPercentage=40 # 初始堆占容器内存的40%
-XX:MaxRAMPercentage=70 # 最大堆占比(为堆外留30%冗余:Metaspace/Direct/Stack/CodeCache)# G1 关键目标与触发
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 期望最大停顿(起点值,按业务调)
-XX:InitiatingHeapOccupancyPercent=30 # 并发标记启动阈值(IHOP)
-XX:G1NewSizePercent=20 # 新生代最小占比
-XX:G1MaxNewSizePercent=60 # 新生代最大占比
-XX:G1ReservePercent=20 # 预留比例,避免晋升失败
-XX:+ParallelRefProcEnabled # 并行引用处理,加速回收# 线程与 CPU(容器内强制核数时建议显式声明)
-XX:ActiveProcessorCount=<容器核数> # 避免 JVM 误判可用 CPU
# -XX:ConcGCThreads=<n> -XX:ParallelGCThreads=<m> # 如需精控再设# 预触页(可选:减少首突刺)
# -XX:+AlwaysPreTouch# 可观测性(统一 GC 日志 + JFR)
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app
-XX:ErrorFile=/var/log/app/hs_err_pid%p.log
# (可选)-XX:StartFlightRecording=name=boot,settings=profile,dumponexit=true,filename=/var/log/app/boot.jfr
说明要点:
- 堆百分比让容器下更稳,堆外留 25–35% 空间(Metaspace/直接内存/线程栈/CodeCache)。
MaxGCPauseMillis
是目标非硬 SLA,调太低会牺牲吞吐。IHOP/G1NewSize%/G1MaxNewSize%/G1Reserve%
是实战常用的四件套。
3) ZGC 启动参数基线(JDK 17+)
-XX:+UseZGC
-XX:+ZUncommit # 允许空闲堆回收给 OS(JDK17+ 多为默认,可显式)
-XX:SoftMaxHeapSize=<size or %> # “软上限”:倾向于把堆压到这个量,闲时回退
-XX:ZUncommitDelay=300s # 空闲多少秒后开始uncommit
# -XX:+ZGenerational # JDK21+ 代际 ZGC(可选,视版本/稳定性启用)# 容器与线程
-XX:InitialRAMPercentage=40
-XX:MaxRAMPercentage=70
-XX:ActiveProcessorCount=<容器核数>
# -XX:ConcGCThreads=<n> # 如需限制并发 GC 线程再设# 观测
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app
-XX:ErrorFile=/var/log/app/hs_err_pid%p.log
# (可选)-XX:StartFlightRecording=name=boot,settings=profile,dumponexit=true,filename=/var/log/app/boot.jfr
ZGC 无显式“暂停目标”,上线验收看 3 项:
- 暂停分布:JFR/
-Xlog
里 GC Pause P99 ≤ 10–20 ms(按业务 SLA 设阈值),且尾部稳定无长尾尖峰。 - 分配与并发周期:Allocation rate 在峰值下 无明显 pacing/Allocation Stall;单次并发周期耗时稳定(秒级)且不频繁回退到 Full。
- 资源占用:GC 线程 CPU 占比可控(常 <5–10%),RSS 随闲忙可回落(验证
SoftMaxHeapSize
与ZUncommit
生效)。
Q22题(线程 Dump 状态判读与处置)
RUNNABLE
、BLOCKED
、WAITING/TIMED_WAITING
、PARKED
、NATIVE
五类线程在 Thread.print/jstack 中常见的栈顶方法特征各 2 个;- 各状态对应的可能根因与快速处置动作(各给 1–2 条);
- 如何把
top -H
的 十进制 TID 映射到Thread.print
的nid=0x..
(写出关键命令)。
1) 常见线程状态的“栈顶方法特征”(各 2 个)
-
RUNNABLE
- 纯计算热点:
java.util.*
(排序/流式聚合/正则/JSON)、BigDecimal
/压缩/加密等方法反复出现。 - 伪 RUNNABLE 的阻塞 I/O:
java.net.SocketInputStream.socketRead0
、sun.nio.ch.EPollArrayWrapper.epollWait
(显示 RUNNABLE,但实为内核阻塞)。
- 纯计算热点:
-
BLOCKED(等待进入 monitor)
- waiting to lock <0x...>
,栈含synchronized
临界区方法。java.util.concurrent.locks.ReentrantLock$NonfairSync.acquire
/AbstractQueuedSynchronizer.acquire
。
-
WAITING / TIMED_WAITING
java.lang.Object.wait(Native Method)
/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await
。java.lang.Thread.sleep
(TIMED_WAITING)、java.util.concurrent.CompletableFuture$Signaller.block
。
-
PARKED(LockSupport)
jdk.internal.misc.Unsafe.park
/sun.misc.Unsafe.park
(LockSupport.park/parkNanos
)。- 线程池/队列空闲:
java.util.concurrent.ForkJoinPool.awaitWork
、java.util.concurrent.SynchronousQueue$TransferStack.transfer
。
-
NATIVE(本地栈热点/JNI)
- I/O 原生调用:
read0
/write0
/poll
/epollWait
/accept0
。 - JNI/库调用:
com.xxx.NativeLib.doCall(Native Method)
、Unsafe.copyMemory
等。
- I/O 原生调用:
2) 可能根因与快速处置
-
RUNNABLE
- 根因:CPU 计算热点/死循环,或 I/O 阻塞被标成 RUNNABLE。
- 处置:用 JFR 看 Hot Methods/Allocation;优化算法或加退避;若是下游 I/O 慢→校验超时/重试/限流。
-
BLOCKED
- 根因:锁竞争/锁顺序反转。
- 处置:根据
waiting to lock
的 owner 对齐热点锁,缩小临界区、换无锁/分段锁;必要时先降并发。
-
WAITING / TIMED_WAITING
- 根因:条件等待/超时等待、线程间通信不匹配。
- 处置:核对通知/超时/队列长度,避免无限等待;检查上游是否“喂不饱”导致饥饿。
-
PARKED
- 根因:线程池空闲/阻塞在工作队列、限流/背压。
- 处置:核对任务提交/取出速率、队列容量;避免过度并发造成反压失衡。
-
NATIVE
- 根因:JNI/文件/网络/磁盘原生调用慢或挂起。
- 处置:加超时与熔断;定位具体
.so
/系统瓶颈(磁盘/网络),必要时降载。
3) 把 top -H
的十进制 TID 映射到 Thread.print
的 nid=0x..
# ① 找高 CPU 线程
top -H -p <PID> # 记下十进制 TID(如 1234)# ② 转 16 进制
printf '0x%x\n' 1234 # => 0x4d2# ③ 抓线程栈并定位
jcmd <PID> Thread.print > /tmp/th.txt
sed -n '/nid=0x4d2/,/^$/p' /tmp/th.txt # 打印对应线程的栈段
Q23(Heap Dump 精确分析:最小流程)
- 何时抓 dump:给出 3 个触发阈值/时机(如 FGC 后 Old 不回落、Top-K 类直方图 3 次单调上升、Metaspace 高位等),以及为何选低峰。
- 如何抓 dump:写出命令(优先
jcmd <pid> GC.heap_dump filename=...
),并说明磁盘/权限/暂停风险注意事项。 - 用什么看:列出 3 个核心视图与提问(如 Dominator Tree 定位最大保留、Leak Suspects Report、Path to GC Roots 的强/软引用链),以及各自产出什么结论。
- 从证据到修复:给出 3 条典型修复动作(如移除静态集合引用、修 ThreadLocal 泄漏、缩小缓存/加 TTL、流式化/分页等),并说明如何复验(哪些指标应改善)。
1) 何时抓 dump(触发阈值/时机 ×3+;为何选低峰)
- 老年代不回落:
FGC
连续发生且O
(Old)在 FGC 后回落 < 5–10%,持续 2–3 个周期。 - 直方图单调上升:
jcmd GC.class_histogram
每 20–30s 采 3 次,同一 Top-K 类的#bytes/#instances
持续上行(尤其跨 FGC 后仍上行)。 - Metaspace 高位:
M/CCS
使用率 ≥85% 且仍上行;或 GC 日志出现Metadata GC Threshold
。 - (可选)业务异常:
Allocation Failure
、To-space exhausted
、SLO 急剧恶化。 - 选低峰的原因:堆 dump 会 进入安全点/可能长暂停 + 大体量磁盘写入,容易抖服务;低峰更安全。
2) 如何抓 dump(命令 + 注意事项)
# 首选(较安全):jcmd
jcmd <PID> GC.heap_dump filename=/data/dump/app-$(date +%F-%H%M).hprof# 兜底(更重):jmap
jmap -dump:format=b,file=/data/dump/app-$(date +%F-%H%M).hprof <PID>
# 若进程无响应才考虑 -F:jmap -F -dump:format=b,file=...
- 磁盘:预留 ≥ 堆大小 的空间(建议 1–2×);写到本地 SSD 或已挂载的持久卷。
- 权限:目录可写;容器内用
kubectl exec
,必要时先mkdir -p /data/dump && chmod 700
。 - 暂停风险:
heap_dump
会进 Safepoint;jmap -F
风险更大,慎用。 - 完成后:
gzip -9 app-*.hprof
压缩、校验md5
,避免跨环境泄漏隐私数据。
3) 用什么看(核心视图 ×3 与提问)
-
Dominator Tree(支配树 / Retained Size)
- 问:谁在“最大保留”内存?哪些对象/容器(如
byte[]
、char[]
、HashMap
、ConcurrentHashMap
)支配了大量内存? - 产出:定位内存根因集合与其拥有者(类、单例、缓存、会话等)。
- 问:谁在“最大保留”内存?哪些对象/容器(如
-
Path to GC Roots(到根路径,排除软/弱)
- 问:这些大对象为何不可回收?强引用链是不是静态字段、线程(含 ThreadLocal)、ClassLoader?
- 产出:给出精确引用链,能落到代码处置点。
-
Leak Suspects / Histogram(嫌疑报告 / 直方图)
- 问:是否存在“疑似泄漏分量”;哪几类的
#bytes/#instances
最大? - 产出:快速锁定嫌疑类型与实例分布;用 Histogram 验证堆内“谁最大”。
- 问:是否存在“疑似泄漏分量”;哪几类的
常用工具:Eclipse MAT、JMC Heap Analyzer、YourKit。MAT 中可用 OQL 进一步过滤(比如某 Map 的 key 前缀)。
4) 从证据到修复(典型动作 ×3;如何复验)
-
移除长寿命强引用:清理/缩小静态集合/单例缓存;引入 Caffeine 限额/TTL/最大权重。
-
修 ThreadLocal 泄漏:请求完成后
finally { tl.remove(); }
;避免在线程池中遗留大对象。 -
流式化 / 分页:避免一次性加载/拼接巨量数据;改为分页/分块/流式处理,复用缓冲。
-
(可选)类加载器泄漏:确保插件/热更 ClassLoader close() 并断开静态回指。
-
复验:
jstat -gcutil <PID> 1000 120
:YGC 频度下降、GCT
斜率变小、O
保持稳定/更低。- 再抓一次 Histogram 对比:嫌疑类型的
#bytes/#instances
不再单调上升。 - 业务 SLO:P95/P99 延迟恢复,GC 日志暂停分布改善。
Q24(ClassLoader 泄漏定位与修复)
- 线上判定 ClassLoader 泄漏 的 3 个信号(含一个来自 堆分析、一个来自 NMT/内存、一个来自 运行现象)。
- 用 MAT/JMC 从 dump 到“定位哪个 ClassLoader 保留了哪些类/对象”的 3 步法。
- 代码与配置两侧给出 4 条修复建议(如
URLClassLoader#close()
、解除静态回指、隔离缓存、热更策略等),并说明各自副作用或权衡。
1) 线上判定 ClassLoader 泄漏 的 3 个信号
-
堆侧(来自 dump):在 MAT 的 Dominator Tree 中,
*ClassLoader
实例及其Retained Size 特别大;Path to GC Roots
显示它被静态字段/线程/ThreadLocal 强引用,导致其加载的类对象不可回收。 -
NMT/内存侧:开启 NMT 后
jcmd <pid> VM.native_memory summary jcmd <pid> VM.native_memory baseline ; sleep 60 ; jcmd <pid> VM.native_memory summary.diff
观察 Class/Metaspace 类目
committed
持续上涨;或jstat -class <pid> 1000 10
看到 Loaded 单向上升、Unloaded 很少。 -
运行现象:GC 日志频繁出现
Metadata GC Threshold
,Metaspace
使用率高位并继续爬升,最终可能OutOfMemoryError: Metaspace
;多次热加载/灰度后常复现。
2) 用 MAT/JMC 从 dump 到“哪个 ClassLoader 在保留谁”的 3 步
- 锁定 Loader:MAT → Histogram 搜索
*ClassLoader
,排序看 Shallow/Retained Size 最大的 Loader;或用 Class Loader Explorer 视图(若有)。 - 看支配关系:对该 Loader 点 “Merge Shortest Paths to GC Roots (exclude soft/weak)” 或直接在 Dominator Tree 中展开,确定是 静态单例/线程/ThreadLocal 等在保留它。
- 落到类型与实例:右键 “List Objects → with incoming references” 找到导致保留的具体字段/集合;并在 Histogram 中按 Loader 过滤,看由它加载的重复类/大数组/缓存等热点类型。
备选:在线快速统计
jcmd <pid> VM.classloader_stats # 各类加载器已加载类计数/占用(不同 JDK 版本命令名可能略有差异)
jcmd <pid> VM.system_properties | grep -i metaspace
3) 代码与配置侧的 4 条修复建议(含副作用/权衡)
- 关闭并释放可卸载 Loader:对自建/插件化的
URLClassLoader
调用close()
,停止由该 Loader 创建的后台线程,清理 ThreadLocal;
副作用:动态加载功能/热更时机需重新设计,关闭过早会影响运行中的模块。 - 断开静态回指:避免从父加载器的单例/缓存持有子加载器对象或其类实例;将可变缓存下沉到子加载器私有范围;
副作用:需要梳理全局单例,拆分缓存作用域。 - 控制动态生成类/代理(CGLIB/ASM/Javassist/脚本引擎)数量与生命周期,复用
ClassLoader
或在卸载前批量清理;
副作用:灵活性下降,可能影响热扩展能力。 - 规避 ThreadLocal 泄漏:业务线程池中使用的
ThreadLocal
在请求完成后统一remove()
;避免将应用类实例放入父加载器可见的单例/ThreadLocal;
副作用:需要编码规范与审计,改造成本。
运行参数层面(重启生效):增大
-XX:MaxMetaspaceSize
只能买时间,不能治本;务必配合修复引用链。
Q25(统一日志进阶:如何量化“是否 GC 绑定”) 给出一段可操作的量化判据与命令组合:
- 你如何用
-Xlog:gc*,safepoint
(或 JDK8 GC 日志) 在 5 分钟窗口内量化“GC 时间占比 ≥ 30% 即判定 GC 绑定”?写出日志开启方式与一条 awk/grep 统计思路; - 若不是 GC 绑定,你会用 一条
jstat -gcutil
+ 一条Thread.print
的节奏完成CPU/锁初判(写明采样频率/时长与看点)。
1) 用 GC 统一日志量化“5 分钟窗口 GC 占比 ≥30% 即判定 GC 绑定”
开启日志(JDK 11+,推荐带 uptime 装饰器便于计算):
# 启动期
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:uptime,level,tags:filecount=5,filesize=20M# 运行期动态开启
jcmd <pid> VM.log what=gc*,safepoint decorators=uptime,level,tags \filename=/var/log/app/gc.log filecount=5 filesize=20M
5 分钟窗口统计(awk 思路;以 Pause ... <xx>ms
累加为例):
LOG=/var/log/app/gc.log
END=$(awk -F'[][]' 'END{u=$2; sub(/s/,"",u); print u}' "$LOG") # 最新 uptime 秒
awk -v end="$END" -F'[][]' '
/\[gc.*\] GC\([0-9]+\) Pause/ {up=$2; sub(/s/,"",up);if (up >= end-300) { dur=$NF; sub(/ms$/,"",dur); sum+=dur } # 累加 ms
}
END {gc_sec = sum/1000.0; ratio = 100*gc_sec/300.0;printf "GC_TIME=%.3fs, WINDOW=300s, RATIO=%.1f%% -> %s\n",gc_sec, ratio, (ratio>=30?"GC绑定(≥30%)":"非GC绑定(<30%)");
}' "$LOG"
说明:使用
uptime
装饰器可避免解析日期;若需要把 Safepoint 停顿也算入,可再加一条规则累加 safepoint 的 “Total x ms” 行。
2) 非 GC 绑定时的一条 jstat
+ 一条 Thread.print
初判(节奏与看点)
-
jstat -gcutil <pid> 1000 60
(每 1s × 60 次)- 看 ΔGCT/Δt ≈ 0 或很小(如 <0.05 s/s),
FGC
不增长、O
稳定 ⇒ 不像 GC 瓶颈。
- 看 ΔGCT/Δt ≈ 0 或很小(如 <0.05 s/s),
-
jcmd <pid> Thread.print > /tmp/th1.txt ; sleep 2 ; jcmd <pid> Thread.print > /tmp/th2.txt
- 连续两次对比:若同一批 RUNNABLE 线程栈稳定在业务/JSON/正则/计算方法 ⇒ CPU 计算热点;
- 若大量 BLOCKED/parking 堆在同一锁/队列 ⇒ 锁竞争/背压;
- 如需精确映射高 CPU 线程:
top -H -p <pid>
取 TID →printf '0x%x\n' <tid>
→ 在Thread.print
里搜nid=0x...
。
Q26(统一日志进阶:Safepoint 长停顿量化)
- 用
jcmd VM.print_safepoint_statistics -verbose
与-Xlog:safepoint
,在 5–10 分钟窗口内量化 “Time to safepoint 占比” 与 “VM operation 耗时 Top-N” 的方法(给出命令与一条 awk/grep 统计思路); - 当 到点耗时占比高 时你会优先怀疑哪三类根因,并各给一条快速验证思路;
- 给出两条当场止血与两条中期修复(分别说明副作用)。
1) 5–10 分钟窗口内量化方法(两条路线)
A. 用 jcmd VM.print_safepoint_statistics -verbose
做“前后快照差分”
PID=<pid>
# 取第1次快照
jcmd $PID VM.print_safepoint_statistics -verbose > /tmp/sp1.txt
sleep 300 # 5分钟;或 600=10分钟
# 取第2次快照
jcmd $PID VM.print_safepoint_statistics -verbose > /tmp/sp2.txt# 计算窗口内【TtS 占比 = Δ(Time to safepoint)/Δ(Stopped total)】与 VM Operation Top-N
awk 'FNR==NR{if($0~/Total time for which application threads were stopped:/){t1=$NF; sub(/ms/,"",t1)}if($0~/Time to safepoint:/){s1=$NF; sub(/ms/,"",s1)}next}{if($0~/Total time for which application threads were stopped:/){t2=$NF; sub(/ms/,"",t2)}if($0~/Time to safepoint:/){s2=$NF; sub(/ms/,"",s2)}}END{dt=t2-t1; ds=s2-s1; r=(dt>0?100*ds/dt:0);printf "WINDOW=%ds Stopped=%.3fs TtS=%.3fs TtS-RATIO=%.1f%%\n", 300, dt/1000, ds/1000, rif(r>=30) print "=> TtS占比过高(≥30%),优先查“到点困难”。"}
' /tmp/sp1.txt /tmp/sp2.txt# 统计该窗口 VM Operation 的 Top-N(按Total耗时)
awk 'BEGIN{in=0}/VM Operation/ && /Count/ && /Total/ {in=1; next}in && NF==0 {in=0}in && NF>=3 {op=$0; total=$(NF-1); sub(/ms/,"",total);m[op]+=total}END{for(k in m) printf("%10.1f ms %s\n", m[k], k) | "sort -nr | head -5"}
' /tmp/sp2.txt
解读:
ΔStopped
= 窗口内应用线程总停顿;ΔTtS
= 窗口内到达 safepoint 的耗时。- 判据:
ΔTtS / ΔStopped ≥ 30%
⇒ 大量时间耗在“等线程进安全点”,多半是坏公民线程长时间不检查轮询点/在 native 中。
B. 用统一日志 -Xlog:safepoint
做“时间窗聚合”
# 在线开启(JDK 11+)
jcmd $PID VM.log what=safepoint decorators=uptime,level,tags \filename=/var/log/app/safepoint.log filecount=5 filesize=20M# 以最后5分钟为窗口,累加 stopped 与 TtS
LOG=/var/log/app/safepoint.log
END=$(awk -F'[][]' 'END{u=$2; sub(/s/,"",u); print u}' "$LOG")
awk -F'[][]' -v end="$END" '
/Total time for which application threads were stopped:/ {up=$2; sub(/s/,"",up);if (up>=end-300) { x=$0; sub(/.*stopped: /,"",x); sub(/ ms.*/,"",x); stopped+=x }
}
/(stopping time|to safepoint)/ { # 不同JDK文案略有差异up=$2; sub(/s/,"",up);if (up>=end-300) { y=$0; sub(/.*(time|safepoint): /,"",y); sub(/ ms.*/,"",y); tts+=y }
}
END{ printf "Stopped=%.3fs TtS=%.3fs TtS-RATIO=%.1f%%\n", stopped/1000, tts/1000, (tts>0?100*tts/stopped:0)}
' "$LOG"
备注:不同版本日志关键字可能是
stopping time
或time to safepoint
,上面用正则兼容。
2) 若 TtS 占比高,优先怀疑的三类根因 & 快速验证
-
JNI/系统调用卡住(线程长时间在 native,不进 safepoint)
- 验证:
jcmd Thread.print
观察大量线程栈顶为socketRead0/read/write/epollWait/accept0
或第三方Native Method
;必要时top -H
取 TID →strace -fp <tid>
看到系统调用久返。
- 验证:
-
长计算循环缺轮询点(热循环几乎不触发 safepoint poll)
- 验证:多次
Thread.print
同一批RUNNABLE
业务栈稳定不变;JFR Hot Methods 占比极高而ΔGCT/Δt≈0
(非 GC)。
- 验证:多次
-
GC Locker/JNI Critical 区域(如
GetPrimitiveArrayCritical
持有过久)- 验证:JFR/栈里频见
*Critical
路径或相关 native;(可选)打开-Xlog:gc+locker=info
看是否频繁 “GC locker is occupied”。
- 验证:JFR/栈里频见
其他:频繁的 ThreadDump/RedefineClasses 会拉高VM operation time(而非 TtS 本身),但同样会扩大总停顿,需一并留意。
3) 当场止血 ×2 & 中期修复 ×2(含副作用)
当场止血(不重启优先)
- 限流/降级/暂停大任务:压低制造“坏公民线程”的入口(大报表、压缩/加密、重 I/O/大 JSON)。
副作用:吞吐/功能下降,但能迅速降低 TtS。 - 停止外部重操作:暂停高频
jstack
/kill -3
/APMThreadDump
、热更/重定义(RedefineClasses
)。
副作用:可观测性与调试手段受限。
中期修复(多需重启或改代码)
- 缩短 native/计算临界段:把长 JNI/系统调用切小、加超时与中断;对热循环引入可中断点/轮询点或优化算法。
副作用:代码改造成本;可能影响性能路径,需要压测。 - 参数层面缓解 TtS 极端值:重启加
-XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=2000
(2s)
作为“安全阀”限制最大 TtS。
副作用:轻微额外开销;治标不治本,仍需修正根因。
Q27(NMT 原生内存跟踪 · 堆外内存快速定位)
-
前置条件与开销:开启 NMT (Native Memory Tracking) 需要哪些启动参数?它的典型开销多大、什么时候不适合长期开启?
-
三步操作法:写出用
jcmd VM.native_memory
进行baseline → 等待 → summary.diff 的三条命令,并说明每一步你在看什么字段(至少列出Thread/Class(Code)/Internal(Other)/GC
其中三类)。 -
三种典型场景的判读与动作(各给 1 条判据 + 1 条当场动作):
- 线程栈上涨(
Thread
类目) - Metaspace 上涨(
Class
类目) - 直接内存/JNI 涨(
Internal/Other
类目)
- 线程栈上涨(
-
如果启动时没开 NMT:给出两条无侵入替代方案(例如使用哪些命令/指标组合来侧证“堆外在涨”),并各说明其局限性。
1) 前置条件与开销
-
启动参数:
-XX:NativeMemoryTracking=summary
(低开销,适合长开)或-XX:NativeMemoryTracking=detail
(更细分项,开销更高)。- 建议加统一日志/JFR等可观测性参数,但 NMT 开关必须在启动时指定。
-
典型开销:
summary
通常 ~1–2% CPU/内存;detail
可能 ≥5%,并扩大元数据开销。 -
不宜长期开:高并发/低配环境、对极致吞吐敏感的服务不建议长期用
detail
;需要时段性开启或灰度。
2) 三步操作法(baseline → 等待 → diff)
# 1) 设基线
jcmd <pid> VM.native_memory baseline# 2) 等待 30~120s(视业务)
sleep 60# 3) 对比增长
jcmd <pid> VM.native_memory summary.diff
# 如需总览
jcmd <pid> VM.native_memory summary
-
关注字段(至少三类):
- Thread:线程相关(线程栈/结构体);上涨=线程数增或栈太大。
- Class(=Metaspace):类元数据;上涨=类加载/类加载器可能泄漏、热更频繁。
- Code:JIT 代码缓存;逼近上限要警惕 Code Cache 满。
- Internal/Other:常见 DirectByteBuffer/Netty/JNI 归入此类,持续上涨警惕堆外泄漏。
- GC/Compiler/Symbol/Arena:作为次要参考,异常增长需结合版本与日志。
3) 三种典型场景的判读与当场动作
-
线程栈上涨(Thread)
- 判据:
summary.diff
中 Thread committed 持续增长;ps -L -p <pid> | wc -l
线程数也在涨。 - 动作:临时 下调线程池并发/限流,排查线程泄漏;必要时降低每线程栈(重启后
-Xss
,谨慎)。
- 判据:
-
Metaspace 上涨(Class)
- 判据:
Class(Metaspace) committed
单向上行;jstat -class <pid> 1000 10
显示 Loaded 增、Unloaded 少;GC 日志见Metadata GC Threshold
。 - 动作:暂停热更/字节码重定义、排查 ClassLoader 泄漏;短期限流相关功能。
- 判据:
-
直接内存/JNI 涨(Internal/Other)
- 判据:
Internal/Other committed
增长;同时 Java 堆稳定但 RSS 上涨。 - 动作:限流/降并发;(Netty)trim 线程本地缓存、控制单次消息大小;触发一次
jcmd <pid> GC.run
仅回收已不可达的 DirectBuffer(治标)。
- 判据:
4) 若启动没开 NMT:两条无侵入替代与局限
-
RSS vs 堆对比
jstat -gcutil <pid> 2000 10 # 堆与 GCT 斜率小 cat /proc/<pid>/status | egrep 'VmRSS|VmSwap' cat /proc/<pid>/smaps_rollup | egrep 'Rss|Pss|Swap'
结论:堆稳定而 RSS 持续上行 ⇒ 堆外在涨。
局限:无法分解到 Thread/Direct/Code 等类别,只能宏观判断。 -
JMX BufferPool(Direct)/线程数/CodeCache 侧证
- 直连 JMX 看
java.nio:type=BufferPool,name=direct
的MemoryUsed/Count
是否上行; ps -L -p <pid> | wc -l
侧证线程数;-Xlog:codecache=info
或jcmd <pid> VM.log what=codecache=info
看 Code Cache 使用率。
局限:JMX 未必可用;跨组件来源仍需结合日志/JFR判断。
- 直连 JMX 看
Q28(JFR 预设与低开销取证 · 配置与验收)
-
JFR 预设差异:说明
settings=default / profile / continuous
三者的覆盖范围与开销差异,各自适用场景各举 1 条。 -
两套落地“配方”:
- 短时取证(2–3 分钟,CPU+内存分配热点并重):写出完整命令(
JFR.start
/JFR.dump
/JFR.check
/JFR.stop
)与关键参数(如filename
、stackdepth
、samplethreads
),并说明预期开销。 - 低扰动巡检(30 分钟以上):写出一套更稳的配置(例如
settings=default
、较小stackdepth
、周期性dump
),并说明如何限速磁盘占用。
- 短时取证(2–3 分钟,CPU+内存分配热点并重):写出完整命令(
-
上线验收:列出 3 个验收指标/阈值(例如:GC Pause P99、Allocation rate 上限、Hot methods 占比/锁等待尖峰),说明达到何种水平可认为“JFR 录制对业务无明显副作用”。
1) JFR 预设差异与适用场景
settings=default
:低开销、事件集精简(基础 GC/线程/I/O/异常等);常开巡检/长期留痕。开销通常 ~0.5–1%。settings=profile
:覆盖更广,含方法采样/内存分配采样等;短时定位 CPU/分配热点。开销一般 ~1–3%。settings=continuous
:面向持续录制的低扰动配置(有的平台/团队自定义此预设;若无,可用default
并下调采样参数实现类似效果);长时间常驻。开销接近 default。
2) 两套落地配方
A. 短时取证(2–3 分钟,CPU + 分配热点)
# 启动录制(180s),到期自动落盘
jcmd <PID> JFR.start name=triage settings=profile duration=180s \stackdepth=128 samplethreads=true \filename=/tmp/triage.jfr dumponexit=true# 中途查看状态
jcmd <PID> JFR.check# 需要即时快照(不中断录制)
jcmd <PID> JFR.dump name=triage filename=/tmp/triage-snap.jfr# 如需提前结束
jcmd <PID> JFR.stop name=triage
预期开销:约 1–3% CPU,对吞吐影响可忽略到轻微;适合问题窗口快速取证。
B. 低扰动巡检(≥30 分钟,控制磁盘占用)
# 常驻录制,滚动控制大小/时长
jcmd <PID> JFR.start name=survey settings=default disk=true \stackdepth=64 samplethreads=false \maxage=30m maxsize=256m \filename=/var/log/app/survey.jfr dumponexit=true
# 可按需周期 dump 一份
jcmd <PID> JFR.dump name=survey filename=/var/log/app/survey-$(date +%H%M).jfr
思路:用 default
+ 较小 stackdepth
、关闭线程采样以降扰动;用 maxage/maxsize
限速磁盘,必要时外部轮转/压缩。
3) 上线验收(满足以下 3 项可认为无明显副作用)
- GC 暂停 P99:与基线相比 增加 < 5 ms 或 < 5%(二者取较松者)。
- CPU/吞吐:进程整体 CPU 增幅 < 2%,QPS 或 P99 延迟变化 < 3–5%。
- 分配/锁:
Allocation rate
与Java Monitor Blocked
P99 无显著上扬(与基线相比变化 <10%)。
Q29(TLAB / Outside TLAB(TLAB 外分配) 分配监控与优化)
- 解释 TLAB 与 Outside TLAB 的区别;用 JFR 或
-Xlog:gc+tlab=debug
(JDK11+) 怎么观察它们的分配量? - 在“频繁 Young GC”场景下,Outside TLAB 比例过高说明了什么?给出 3 条可能原因 与相应 优化动作(如对象过大、TLAB 太小、跨线程发布等)。
- 写出一条最小可行命令组合(JFR 或日志)与判定阈值,用来在 2–3 分钟内快速判断“是否需要针对 TLAB 做调优或代码侧整改”。
1) 概念与观测
-
TLAB 内分配:每个线程在 Eden 拿到一块 Thread-Local Allocation Buffer,走“指针递增”快速分配,几乎无锁。
-
TLAB 外分配(Outside TLAB):没有在本线程的 TLAB 中完成的分配,走 全局慢路径(直接 Eden/老年代/巨大对象路径)。常见于对象太大或 TLAB 剩余不足。
-
如何观察(任选其一):
-
JFR(推荐):
Object Allocation in TLAB
与Object Allocation Outside TLAB
两类事件;在 JMC 的 Allocation 视图可看到 In TLAB vs Outside TLAB 的字节/速率/方法/类型分布。 -
统一日志(JDK 11+):临时开启
jcmd <pid> VM.log what=gc+tlab=debug decorators=uptime,level,tags output=stdout
观察 TLAB 大小、refill 次数、waste 等(不同版本文案略有差异)。
-
JDK 8:可用
-XX:+PrintTLAB
(需重启)配合-XX:+PrintGCDetails
查看 TLAB 统计。
-
2) “频繁 Young GC + TLAB 外分配比例高”通常意味着什么?(原因 → 动作)
- 原因A:对象过大/巨对象走慢路径(如大
byte[]
/char[]
)
→ 动作:分块/流式处理、复用缓冲区;避免一次性构造超大数组/字符串。 - 原因B:TLAB 与分配粒度不匹配(TLAB 太小、补给后剩余经常不够下一次分配)
→ 动作:首先依赖-XX:+ResizeTLAB
(默认开) 的自适应;如仍异常,可在压测环境尝试增大 Young 或调高MinTLABSize
(重启生效,谨慎)。 - 原因C:逃逸/跨线程发布导致大量 Outside 路径(慢路径频繁触发)
→ 动作:减少跨线程创建并发布的大对象;将对象在使用线程内创建与复用,避免在提交给线程池前构造大对象。 - 原因D:频繁构造短命临时对象(导致 TLAB 快速耗尽)
→ 动作:消减临时对象(复用StringBuilder
/byte[]
,集合预尺寸,少用链式中间对象)。
备注:G1 巨对象(humongous)一定走 TLAB 外分配路径;降低发生率的办法是避免巨对象或调整RegionSize(
-XX:G1HeapRegionSize
,重启项,不建议轻易动)。
3) 最小可行判定(2–3 分钟)
命令组合(二选一):
-
JFR 方案(低侵入)
jcmd <pid> JFR.start name=tlab settings=profile duration=180s \stackdepth=64 samplethreads=true filename=/tmp/tlab.jfr
导入 JMC → Allocation 视图 → 读 In TLAB vs Outside TLAB 的字节占比与 Top 方法/类型。
-
统一日志方案(即席观察)
jcmd <pid> VM.log what=gc+tlab=debug decorators=uptime,level,tags output=stdout # 配合 jstat 观测GC趋势 jstat -gcutil <pid> 1000 180
判定阈值(经验)
- 连续 2–3 分钟窗口里,Outside TLAB 字节占比 > 20–30% 且 YGC 频繁(
YGC
快速递增、E
高位反复清空) ⇒ 倾向 TLAB 不匹配/大对象过多/逃逸问题,需要代码整改或 TLAB/Young 调优; - 若 Outside TLAB > 50% 且伴随分配速率高(JFR 里
Allocation rate
明显) ⇒ 优先代码侧治理(消减/分块/复用),参数调优仅作佐助。
Q30(对象年龄分布 / TenuringDistribution 判读)
- 用哪些手段查看对象年龄分布?分别写出 JDK 8 与 JDK 11+ 的最小命令(如
-XX:+PrintTenuringDistribution
、-Xlog:gc+age=trace
)。 - 看到“早晋升(年龄很小就进老年代)”与“Survivor 撑爆”各说明什么问题?各给 2 条可能成因与 1 条调参/代码动作。
- 给出一条最小体检脚本(3–5 行),在 1 分钟内每 5 秒采一次年龄分布,并说明你据此如何判断是否需要调
MaxTenuringThreshold
或 Survivor 比例。
1) 怎么看“对象年龄分布”
-
JDK 8(启动开启)
# 常用最小集(需重启) -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -Xloggc:/var/log/app/gc.log #(部分环境可尝试)动态:jinfo -flag +PrintTenuringDistribution <pid> # 是否支持因发行版而异
日志中会出现:
Desired survivor size ... new threshold N (max M)
与各 age 行。 -
JDK 11+(统一日志,可运行期开启)
# 启动:统一日志 -Xlog:gc+age=trace:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M # 运行期动态(推荐) jcmd <pid> VM.log what=gc+age=trace filename=/var/log/app/gc.log decorators=uptime,level,tags
同样会打印 Desired survivor size / new threshold / age i: 等年龄分布行。
2) 两类典型现象的含义、原因与动作
A. “早晋升”(很小年龄就进老年代)
-
含义:
new threshold
常被压到 1~2,大量对象在 很低年龄 晋升到老年代。 -
可能原因(举 2 条)
- Survivor 太小,无法容纳当次存活对象;
- MaxTenuringThreshold 偏低 或对象存活率偏高(短时间内多次被拷贝)。
-
动作(择一落地)
- 增大 Survivor/新生代(G1:调
G1NewSizePercent/G1MaxNewSizePercent
;并确保 Survivor 容量充足),或适度提高MaxTenuringThreshold
(重启生效,压测评估)。
- 增大 Survivor/新生代(G1:调
B. “Survivor 撑爆/溢出”(to-space 不够)
-
含义:日志显示 survivor 溢出/desired survivor size 远小于实际存活,伴随
new threshold
下降;有时伴 promotion failure 警告。 -
可能原因(举 2 条)
- 瞬时存活对象过多(例如批量组装、串联中间集合);
- 对象过大或 Outside TLAB 比例高,加剧 Eden→Survivor 压力。
-
动作(择一落地)
- 扩大新生代或 Survivor 占比(G1 可调
G1MaxNewSizePercent
;并关注TargetSurvivorRatio
自适应结果),同时代码侧降存活峰值(分页/流式、预尺寸、复用缓冲)。
- 扩大新生代或 Survivor 占比(G1 可调
3) 最小体检脚本(1 分钟 / 每 5 秒采一次)与判定阈值
JDK 11+(统一日志)示例:
PID=<pid>
# 若未开启,先打开年龄分布日志(一次即可)
jcmd $PID VM.log what=gc+age=trace filename=/tmp/gc-age.log decorators=uptime,level,tags# 1分钟窗口采样标记(日志会持续写入)
for i in {1..12}; do date '+%T'; sleep 5; done# 快速摘取最近窗口的关键信息
awk -F'[][]' -v n=200 '/Desired survivor size| age [0-9]+:/ {lines[NR]=$0}END{for(i=NR-n;i<=NR;i++) if(lines[i]!="") print lines[i]}
' /tmp/gc-age.log
判定阈值(经验法)
- 早晋升判据:窗口内连续 ≥3 次
new threshold ≤ 2
; - Survivor 撑爆判据:多次出现 desired survivor size 明显小于当前各 age 总和(或出现“survivor overflow / promotion failure”提示);
- 若同时
jstat -gcutil <pid> 1000 60
显示 YGC 频繁、YGCT 斜率高,则优先考虑扩大新生代/Survivor 与代码侧削峰;若老年代也被顶高,应谨慎评估 MaxTenuringThreshold 调整幅度并先压测。
Q31:VM.flags / VM.command_line 与可热改参数清单
一、怎么查看当前 JVM 启动参数与标志
-
jcmd <pid> VM.command_line
:查看原始启动参数(-Xms/-Xmx/-XX:...
等)。 -
jcmd <pid> VM.flags
:查看所有 JVM 标志及其属性(是否manageable
/writeable
/product
/diagnostic
/experimental
)。- 过滤可热改示例:
jcmd <pid> VM.flags | grep -i manageable
- 过滤可热改示例:
-
(备选)
jinfo -flags <pid>
:JDK8 常用的等价查看方式。 -
相关:
jcmd <pid> VM.system_properties
可核对系统属性(时区、编码等),大多数运行期不可改。
二、哪些能“运行期动态开/关或修改”(不重启)
口诀:日志/JFR/编译指令/极少数 manageable 标志可以动;堆/收集器/区域大小/直接内存上限不可动。
-
统一日志(JDK11+):
- 开:
jcmd <pid> VM.log what=gc*,safepoint decorators=uptime,level,tags filename=/var/log/gc.log filecount=5 filesize=20M
- 停:
jcmd <pid> VM.log disable
- 用途:在线打开/调整
-Xlog
选择器,无需重启。
- 开:
-
JFR 录制:
jcmd <pid> JFR.start ...
/JFR.dump
/JFR.stop
(低开销事件级取证)。
-
编译指令(JIT):
- 排除易爆热点:
jcmd <pid> Compiler.directives_add '[{"match":"com.foo.Bar::hot*","Exclude":true}]'
- 查看/删除:
Compiler.directives_print
/Compiler.directives_clear
- 排除易爆热点:
-
极少数“manageable/writeable”标志(用
VM.flags
确认):- 典型如
MinHeapFreeRatio
/MaxHeapFreeRatio
(调整堆空闲比例阈值,影响堆自动扩缩策略,并非改 -Xmx)。 - 修改方式(JDK9+):
jcmd <pid> VM.set_flag MinHeapFreeRatio 5
- JDK8 可尝试:
jinfo -flag MinHeapFreeRatio=5 <pid>
(是否成功以标志属性为准)。
- 典型如
-
管理代理(JMX):
jcmd <pid> ManagementAgent.start_local
(本地快速开 JMX,非生产长期方案)。
不能动态修改(需重启):
-Xms/-Xmx
、选择收集器(G1/ZGC/CMS)与其大多数调参、G1HeapRegionSize
、MaxMetaspaceSize
、ReservedCodeCacheSize
、MaxDirectMemorySize
、NativeMemoryTracking
开关等。
三、常用“可执行动作”但不是“改参数”
- 触发 GC:
jcmd <pid> GC.run
(仅买时间,可能带 STW)。 - 直方图:
jcmd <pid> GC.class_histogram
(低频用,进入 safepoint)。 - 堆信息:
jcmd <pid> GC.heap_info
;堆 dump:jcmd <pid> GC.heap_dump filename=/path/app.hprof
(低峰执行)。 - 安全点/日志:
jcmd <pid> VM.print_safepoint_statistics -verbose
;VM.native_memory summary
(NMT 需启动开启)。
Q32(统一日志选择器速查 & 提取范式)
-
选择器速查:写出 至少 6 组常用
-Xlog
选择器组合(含等级或分类),并各用半句说明用途(例:gc+phases=debug
、gc+heap=info
、gc+age=trace
、gc*=info
、safepoint=info
、codecache=info
、gc+tlab=debug
等)。 -
运行期开启/关闭范式:给出 2 条你线上常用的
jcmd VM.log
命令(一个落到文件滚动、一个打到 stdout 即席观察),以及如何关闭。 -
5 分钟窗口聚合:给出 1 段 awk/grep 思路(或伪代码),从带
uptime
装饰器的 GC 统一日志里计算最近 5 分钟内:- a) 暂停总时长与事件次数(Young/Full 可分开或合并);
- b) GC 时间占比(暂停总时长 / 300s);
- c) 若开启了
gc+age=trace
,统计最近窗口里出现的new threshold
最小值。
-
阈值判定(给出数字):写出你判定“GC 绑定”的量化标准(例如:暂停占比 ≥ 30% 或 Full GC ≥ 2 次/5 分钟),以及你据此的当场动作(限流/暂停重任务/一次性
GC.run
等)各 2 条。
1) 常用 -Xlog
选择器
gc*=info
:总览 GC 事件与原因、时长。gc+phases=debug
:分阶段耗时(如 G1 Evacuation/Remark/Cleanup)。gc+heap=info
:回收前后堆与各代占用。gc+age=trace
:对象年龄分布/new threshold
。gc+tlab=debug
:TLAB 尺寸/补给/浪费。gc+metaspace=info
:元空间使用与扩容。safepoint=info
:停顿原因、time to safepoint
。codecache=info
:代码缓存使用率/是否接近满。
2) 运行期开启/关闭范式(JDK 11+)
-
落盘滚动
jcmd <pid> VM.log what=gc*,safepoint decorators=uptime,level,tags \filename=/var/log/app/gc.log filecount=5 filesize=20M
-
即席观察到 stdout
jcmd <pid> VM.log output=stdout what=gc+phases=debug
-
关闭
jcmd <pid> VM.log disable
3) 5 分钟窗口聚合(awk 思路)
依赖
decorators=uptime
;统计暂停时长与次数、GC 占比、new threshold
最小值。
LOG=/var/log/app/gc.log
END=$(awk -F'[][]' 'END{u=$2; sub(/s/,"",u); print u}' "$LOG")
awk -F'[][]' -v end="$END" '
/\[gc.*\] GC\([0-9]+\) Pause/ { # 累加暂停up=$2; sub(/s/,"",up); if (up>=end-300) {c++; d=$NF; sub(/ms$/,"",d); sum+=d}
}
/new threshold/ { # 记录最小 new thresholdup=$2; sub(/s/,"",up); if (up>=end-300) {match($0,/new threshold [0-9]+/,m);if(m[0]!=""){split(m[0],a," "); thr=(min==0?a[2]:(a[2]<min?a[2]:min)); min=thr}}
}
END {gc_sec=sum/1000.0; ratio=100*gc_sec/300.0;printf "WINDOW=300s PAUSES=%d GC_TIME=%.3fs RATIO=%.1f%% NEW_THRESHOLD_MIN=%s\n",c, gc_sec, ratio, (min==0?"N/A":min)
}' "$LOG"
4) 阈值判定与当场动作
-
判定为“GC 绑定”(满足任一):
- 5 分钟暂停占比 ≥ 30%;
- Full GC ≥ 2 次/5 分钟;
- Young 平均暂停 > 120 ms 且 YGC 次数 ≥ 30/5 分钟。
-
当场动作(任选两条):
- 限流/暂停重任务(报表/导出/大批处理),降低分配速率;
- 一次性
jcmd <pid> GC.run
在低峰执行,仅作买时间; - 压缩/清空本地缓存、降低日志/序列化开销;
- 短录 JFR 180s 捕捉分配与 GC 证据(
settings=profile
)。
Q33(JIT 快速绕路:Compiler.directives
应急)
- 给出一套在线绕开热点方法的命令:添加、查看、清除
Compiler.directives
;并说明“绕开”的直接效果是什么(对该方法的编译/执行形态)。 - 何时适合用它做应急止血?列出 2 个触发条件(如 code cache 逼近、JIT 疑似崩溃路径)。
- 给出 回滚与验证 的做法(包括如何确认 directives 生效,如何回退到默认编译行为)。
1) 在线绕开热点方法(命令与效果)
# 添加指令:排除某方法的JIT编译(回退解释执行)
jcmd <pid> Compiler.directives_add '[{"match":"com.foo.Bar::hotMethod","Exclude":true}]'# 或限制为仅C1(禁用C2,降低优化强度与CodeCache压力)
jcmd <pid> Compiler.directives_add '[{"match":"com.foo.Bar::*","C1":true,"C2":false}]'# 查看当前指令(含ID)
jcmd <pid> Compiler.directives_print# 移除指定ID(或清空全部)
jcmd <pid> Compiler.directives_remove <ID>
jcmd <pid> Compiler.directives_clear
直接效果:被 Exclude
的方法不再被JIT编译(后续以解释器执行;若已是已编译版本,会在下次去优化/重新计数后不再编译)。设置 C2:false
则让方法最多到C1,减少激进优化与Code Cache占用。
生效验证可配合:
jcmd <pid> VM.log what=compilation=debug
(观察不再出现该方法的编译记录)
jcmd <pid> Compiler.queue
(确认编译队列无该方法)
jcmd <pid> VM.log what=codecache=info
(观察代码缓存增长趋缓)
2) 适用的“应急止血”触发条件(举2条)
- Code Cache 逼近上限或日志出现 “CodeCache is full”/编译队列堆积 → 先排除极热巨型方法,降低新增产物。
- 疑似JIT相关崩溃/异常(
hs_err
的Problematic frame
指向 C2/编译器路径,或Current CompileTask
异常)→ 临时排除涉事方法或禁用C2绕行。
3) 回滚与验证
- 回滚:
Compiler.directives_remove <ID>
或Compiler.directives_clear
恢复默认编译行为。 - 验证:再次
Compiler.directives_print
确认为空;开启-Xlog:compilation=debug
(运行期VM.log
)检查方法重新进入编译;观察codecache=info
使用率曲线与性能是否回归。
Q34(JIT/CodeCache 体检与编译队列)
- 写出你在线上**判断是否出现“编译风暴/队列堆积/CodeCache 压力”**的最小命令组合(≥3 条),并说明各自看点(如
Compiler.queue
、-Xlog:compilation
、-Xlog:codecache=info
)。 - 若确认队列堆积且 CodeCache 使用率>90%,给出 两条不重启止血 与 两条重启后修复(各注明副作用)。
- 用一句话说明为何“编译风暴”会放大请求延迟,即使编译线程是后台线程。
1) 最小命令组合(线上体检)
jcmd <pid> Compiler.queue
看 C1/C2 编译队列长度与待编译方法(持续高位=堆积)。jcmd <pid> VM.log what=compilation=debug output=stdout
(或启动-Xlog:compilation=debug
)
看 编译事件洪峰、同一方法反复编译/去优化(deopt/OSR)。jcmd <pid> VM.log what=codecache=info
看 CodeCache 各段使用率、是否出现 “CodeCache is full”。
佐证:
-Xlog:gc*=info
对齐暂停/安全点;jcmd <pid> VM.flags | grep ReservedCodeCacheSize
核对上限。
2) 止血与修复
不重启止血(任选两条)
-
绕开热点方法的高阶编译
jcmd <pid> Compiler.directives_add '[{"match":"com.xx.Hot*","Exclude":true}]' # 全禁JIT # 或仅禁C2 jcmd <pid> Compiler.directives_add '[{"match":"com.xx.*","C1":true,"C2":false}]'
副作用:该方法退回解释或仅C1,吞吐下降/延迟上升。
-
业务限流/降级
降低触发编译的热点路径调用频率,缓解队列与CodeCache增长。
副作用:SLA/功能受限。
重启后修复(任选两条)
- 增大 CodeCache:
-XX:ReservedCodeCacheSize=256m
(或更大),确保-XX:+UseCodeCacheFlushing
。
副作用:RSS 增、冷启动编译时间可能更长。 - 降低编译强度:
-XX:TieredStopAtLevel=1|2|3
或-XX:-UseC2
(极端可-Xint
做诊断)。
副作用:性能下降,需压测权衡。
3) 一句话解释“编译风暴为何放大延迟”
编译线程虽在后台,但会争用 CPU,并在安装/去优化时触发安全点与指令缓存失效,同时热点方法未编译或被去优化回解释执行,整体吞吐下降、尾延迟上扬。
Q35(综合演练 · 2 分钟内从症状到证据闭环)
情景:线上告警“CPU 高 + Young GC 频繁,老年代稳定”。
- 写出一段 3–5 行命令脚本,在 120 秒 内同时采集:
jstat -gcutil
、两次Thread.print
、以及一段 JFR 180s(即可刻启动)。 - 写出你在 60 秒 内如何判定“计算热点 vs 短命对象暴增”(各给 2 个证据点)。
- 若最终判定是“短命对象暴增”,给出 2 条不重启缓解 与 2 条重启后修复(只围绕第四章工具能指导到的动作)。
1) 120 秒采集脚本(3–5 行即可)
PID=<pid># ① 采 GC 趋势:每 1s 共 120 次
jstat -gcutil $PID 1000 120 > /tmp/gcutil-120s.log &# ② 双采样线程栈(相隔 2s,便于对比是否同一热点)
jcmd $PID Thread.print > /tmp/th1.txt ; sleep 2 ; jcmd $PID Thread.print > /tmp/th2.txt &# ③ 启动低开销 JFR(180s,覆盖 CPU/分配/锁/IO)
jcmd $PID JFR.start name=triage settings=profile duration=180s filename=/tmp/triage.jfr dumponexit=true
以上均为线上低侵入动作;
Thread.print
会进入 safepoint(短暂停),但只做两次,影响可控。
2) 60 秒内判定路径:计算热点 vs 短命对象暴增
A. 计算热点(CPU)
- 证据①:
jstat -gcutil
中 ΔGCT/Δt 很小(GC 时间增长缓慢),但 CPU 高。 - 证据②:两份
Thread.print
中相同 RUNNABLE 线程栈稳定停在 纯计算/序列化/正则/JSON 等方法;导入 JFR,看 Hot Methods 占比居高而 Allocation rate 并不夸张。
B. 短命对象暴增(频繁 Young GC)
- 证据①:
jstat -gcutil
里 YGC 快速递增、E/S0/S1
反复“充满→清空”,YGCT
斜率明显。 - 证据②:JFR 的 Allocation (TLAB/Outside TLAB) 显示 分配速率高、某些类型(如
byte[]/char[]/String
)与方法为热点;(可选)低频jcmd GC.class_histogram
两次对比,Top-K 类型 #bytes/#instances 同窗口上行。
3) 若判定为“短命对象暴增”
不重启缓解(任选两条)
- 限流/降级/暂停重任务:限制导出/大报表/批处理的并发与批量;缩小分页,降低瞬时分配速率。
- 收缩本地缓存与日志开销:下调缓存容量/TTL、降低热路径日志与 JSON 拼接;必要时暂停非关键埋点。
备注:一次性
jcmd $PID GC.run
仅能回收已不可达对象,通常治标,可在低峰尝试。
重启后修复(任选两条,先压测)
- 增大新生代比例/容量:G1 场景调
G1NewSizePercent/G1MaxNewSizePercent
(或等价新生代参数),降低 Young GC 频率。 - 适度提高晋升阈值:
MaxTenuringThreshold
(配合 Survivor 容量),避免早晋升把老年代推高;同时代码侧减少临时对象(复用缓冲、集合预尺寸、流式处理)。
Q36(统一日志 + 年龄分布:快速识别“早晋升/Survivor 撑爆”)
- 给出运行期开启年龄分布日志的命令(JDK 11+,
jcmd VM.log
),并说明要加的 decorators。 - 写一段 awk/grep 思路:从带
uptime
的统一日志中,在最近 5 分钟窗口统计new threshold
的最小值,并判断是否出现“早晋升”(给出你的阈值)。 - 若判定“Survivor 撑爆”,列出 2 条不重启缓解 与 2 条重启后修复(仅限第四章工具能指导到的动作/参数)。
1) 运行期开启年龄分布日志(JDK 11+)
# 开启到文件(推荐带 uptime 便于窗口统计)
jcmd <pid> VM.log what=gc+age=trace \decorators=uptime,level,tags \filename=/var/log/app/gc-age.log filecount=5 filesize=20M# 或即席到 stdout
jcmd <pid> VM.log output=stdout what=gc+age=trace decorators=uptime,level,tags# 关闭
jcmd <pid> VM.log disable
关键 decorators:
uptime,level,tags
(至少uptime
)。
2) 5 分钟窗口统计 new threshold
最小值(awk 思路)
LOG=/var/log/app/gc-age.log
END=$(awk -F'[][]' 'END{t=$2; sub(/s/,"",t); print t}' "$LOG")
awk -F'[][]' -v end="$END" '
/new threshold [0-9]+/{up=$2; sub(/s/,"",up);if (up>=end-300) {if (match($0,/new threshold [0-9]+/,m)) {split(m[0],a," "); v=a[3]+0;if (min==0 || v<min) min=v}}
}
END{printf "NEW_THRESHOLD_MIN(last 5m)=%s\n", (min?min:"N/A")}' "$LOG"
判定“早晋升”阈值(经验):最近 5 分钟 new threshold ≤ 2
多次出现 ⇒ 年龄很小即晋升(Young→Old)。
3) “Survivor 撑爆”——不重启缓解 ×2、重启后修复 ×2
不重启缓解
- 限流/降批/分页:降低瞬时存活量与分配速率(报表/导出/聚合接口)。副作用:吞吐与功能下降
- 一次性 GC(仅买时间):
jcmd <pid> GC.run
在低峰执行;同时 JFR 180s 取证分配热点。副作用:可见 STW
重启后修复
- 增大新生代/Survivor 容量:G1 调
-XX:G1NewSizePercent
/-XX:G1MaxNewSizePercent
(适度上调);必要时检查-XX:TargetSurvivorRatio
。副作用:堆布局变化、吞吐/停顿权衡 - 适度提高晋升阈值:
-XX:MaxTenuringThreshold
(结合 Survivor 容量与对象存活率验证)。副作用:复制成本上升,需压测
Q37(2 分钟识别“早晋升”与“TLAB 外分配”)
- 给出一段 3–5 行命令,120 秒内同时采集:
jstat -gcutil
、VM.log gc+age=trace
、以及一段 JFR 180s。 - 写出你据此在 60 秒内判定“是否早晋升”的 2 个信号(分别来自年龄日志与
jstat
)。 - 若同时观察到 Outside TLAB 占比 > 30%(JFR),给出 2 条代码侧整改 与 1 条参数侧调优(重启项)建议。
1) 120 秒采集(3 行即可)
PID=<pid>
jstat -gcutil $PID 1000 120 > /tmp/gcutil-120s.log &
jcmd $PID VM.log what=gc+age=trace decorators=uptime,level,tags filename=/tmp/gc-age.log filecount=3 filesize=10M &
jcmd $PID JFR.start name=triage settings=profile duration=180s stackdepth=128 samplethreads=true filename=/tmp/triage.jfr dumponexit=true
2) 60 秒内判定“是否早晋升”的两个信号
- 年龄日志信号(gc+age):最近 5 分钟窗口内多次出现
new threshold ≤ 2
(且Desired survivor size
明显小于当次存活字节),说明很小年龄就被晋升。 jstat
信号:在多次 YGC 过程中O
(Old)单调上升而E/S0/S1
反复清零,且YGC
增速快、YGCT
斜率明显 ⇒ 短命对象多、早晋升/Survivor 不足。
3) 若 JFR 显示 Outside TLAB 占比 > 30%:整改建议
-
代码侧(2 条)
- 分块/流式处理:避免一次性分配超大
byte[]/char[]/String
;复用缓冲区(例如复用StringBuilder
/byte[]
)。 - 减少跨线程发布:尽量在实际消费的线程内创建对象,避免在提交到线程池前构造大对象导致 TLAB 外分配与逃逸。
- 分块/流式处理:避免一次性分配超大
-
参数侧(1 条,重启项)
- 适度增大新生代/Survivor 容量(G1:
-XX:G1NewSizePercent
/-XX:G1MaxNewSizePercent
上调;必要时评估-XX:MinTLABSize
),在压测验证后上线。权衡:堆布局变化,吞吐/停顿曲线需复验。
- 适度增大新生代/Survivor 容量(G1: