当前位置: 首页 > news >正文

深入理解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 老年代使用率(%)。
  • MMetaspace 使用率(%,JDK8+)。
  • CCS:Compressed Class Space 使用率(%,JDK8+)。
  • YGC:Young GC 次数(进程启动以来累计)。
  • YGCT:Young GC 累计耗时(秒)。
  • FGC:Full GC 次数(累计)。
  • FGCT:Full GC 累计耗时(秒)。
  • GCT:总 GC 累计耗时(= YGCT + FGCT,秒)。

三种常见场景的快速判读

  1. 频繁 Young GC

    • 现象:YGC 在短时间内快速递增、YGCT 比例在 GCT 中占比高;业务无峰值但 GC 次数高。
    • 佐证:E 经常接近 100% 且迅速清零(频繁回收);对象存活率高导致 S0/S1 也偏高。
    • 方向:加大新生代(如 -Xmn/NewRatio)、优化短命对象创建、检查热点分配点。
  2. 老年代压力过大

    • 现象:O 长期高位(>85%~90%)、FGC 增长、FGCTGCT 中占比升高。
    • 佐证:S0/S1 高 + 快速向老年代晋升(晋升失败风险)。
    • 方向:排查大对象/长寿对象、降低晋升(如调 MaxTenuringThreshold/Survivor 比)、增大老年代
  3. 可能内存泄漏

    • 现象:随着时间推移,O 单调上升,即使发生 FGC不回落GCT 持续增加。
    • 佐证:M/CCS 也缓慢爬升(疑似类加载/类加载器泄漏);GC.class_histogram 显示某些类实例数持续上涨。
    • 方向:在稳定窗口抓堆jcmd GC.heap_dump),比对多次直方图定位“只增不减”的可疑类型;排查缓存/静态集合/线程本地变量/类加载器链路。

Q4: 线上第一时间快速体检 JVM,你会用哪几条命令(至少 3 条),各自看什么,如何根据输出做下一步动作?

  1. 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→按锁顺序修复。
  2. 内存/GC 路径

    • jstat -gcutil <pid> 1000 10:连续 10 秒观察 YGC/YGCTFGC/FGCTO(老年代)是否异常增长。
    • jcmd <pid> GC.heap_info:看堆大小、各代占用与 GC 算法,判断是否逼近阈值。
    • (怀疑泄漏)jcmd <pid> GC.class_histogram:查看实例数/占用增长最快的类
    • 下一步:若老年代高位 + FGC 频繁→先扩容/限流/降级稳住,再在低峰执行 jcmd <pid> GC.heap_dump filename=/tmp/app.hprof 精准分析。
  3. 低开销全景记录(可选)

    • jcmd <pid> JFR.start name=hotfix settings=profile duration=120s filename=/tmp/rec.jfr:两分钟事件级录制
    • 下一步:用 JMC 打开 .jfr热点方法、分配热点、锁等待、IO/Socket 等。

线上首诊的优先级建议top/psjcmd Thread.printjstat -gcutil/jcmd GC.heap_infojcmd 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 栈。

  1. 定位目标进程与高 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 最高的若干个
  1. 把十进制 TID 转成十六进制(映射 nid)
printf '0x%x\n' <TID>
# 例:TID=1234 -> 0x4d2

jcmd Thread.print / jstack 中的线程标识写作 nid=0x...(十六进制)。

  1. 抓取并定位对应 Java 栈
jcmd <PID> Thread.print > /tmp/th.txt   # 或:jstack -l <PID> > /tmp/th.txt
sed -n '/nid=0x4d2/,/^$/p' /tmp/th.txt  # 打印从命中行到空行的该线程栈
  1. 读栈时的关键“信号”
  • 计算热点:线程状态多为 RUNNABLE,栈顶在纯 Java 计算(业务方法、JSON/正则/流式聚合、BigDecimal/加密/压缩等),无明显锁/IO调用。
  • 锁竞争:出现 BLOCKED / waiting to lockparking to wait for,栈中可见 synchronized/ReentrantLock/AbstractQueuedSynchronizer 等;多线程在同一锁处汇聚。
  • IO 卡住:常见 java.net.SocketInputStream.socketRead0sun.nio.ch.*.readFileDispatcherImpl.read 等;NIO 选择器线程可见 Selector.select/EPollArrayWrapper.epollWait(通常 RUNNABLE 但实际阻塞在 native)。
  1. 后续动作
  • 计算热点 → 用 JFR 采样:jcmd <PID> JFR.start settings=profile duration=120s filename=/tmp/rec.jfr
  • 锁竞争 → 栈对齐冲突点,评估拆分锁/缩小临界区/无锁结构;必要时打印多次对比
  • IO 卡住 → 排查下游依赖/网络/磁盘;对热点调用加超时与限流

Q6: 当你怀疑内存泄漏但不方便立刻做堆 Dump 时,如何用 jcmd GC.class_histogram 做“轻量级多次对比”,写出操作节奏(采样次数/间隔)、判读方法(哪些列看什么趋势)、以及何时升级为 GC.heap_dump

目标:在不能马上 dump 的情况下,用低侵入方法确认是否存在“持续增长”的可疑对象。

  1. 基线与配置核对(30–60s)
  • jcmd <pid> VM.flags / VM.system_properties(或 jinfo -flags <pid>):核对 GC、堆大小、Metaspace、是否开启 NMT 等。
  • jstat -gcutil <pid> 5000 12(每 5s,采 1 分钟):观察 OYGC/YGCTFGC/FGCT增长速率与比例;GC.heap_info 快速看堆与代的占用。
  1. 轻量直方图多次对比(建议 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 GCFGC 递增),O 不明显回落(<5%~10%) 且 Top-K 类仍上升 → 泄漏嫌疑增强。
    • M(Metaspace)/CCS 也在爬升,且直方图中类数/字节增长集中于某些动态生成/类加载相关类型 → 考虑 类加载器泄漏
    • Java 堆稳定但 RSS 持续涨 → 用 jcmd <pid> VM.native_memory summary(需 -XX:NativeMemoryTracking=summary|detail)排查 本地内存
  1. 何时升级为 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 进行精确分析。

  1. 频率建议
  • 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

查看 AllocationOld Object(若可用)、Socket/IO锁等待 等事件。


Q7: 如果你只能运行 一条命令 来快速判断“是 CPU 热点还是 GC/内存问题”,你会选哪条?请写出命令与理由,并说明“输出中你最先看的 3 个信号”。

jstat -gcutil <PID> 1000 20

理由jstat -gcutil 能以极低开销提供时间序列 GC 指标,据此快速判断“是否 GC/内存瓶颈”。若 GC 迹象不强,基本可以把问题指向 CPU/锁/IO(再用其它手段深挖)。

最先看的 3 个信号(结合 20 次采样的趋势)

  1. GCT 的斜率 ΔGCT/Δt

    • ≈0(几乎不增长)→ 不是 GC 绑定;更可能是 CPU/锁/IO
    • 明显上升(例如 1s 内涨 0.2s 以上,且持续)→ GC 占用显著,偏向 GC/内存问题
  2. FGC 增长 & O(Old)在 FGC 后是否明显回落**:

    • FGC 递增但 O 不回落或很少回落(<5%~10%)→ 老年代压力/潜在泄漏
    • 几乎没有 FGC,而 O 保持低位→ 多半不是老年代问题。
  3. 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 的触发(满足任一即可):

    1. FGC 递增,但 O(Old)在 FGC 之后不回落或仅回落很少(<5%~10%),并且这一现象跨 3+ 轮连续出现
    2. ΔGCT/Δt 明显(例如 2s 间隔内 GCT 增长 ≥0.4s,且多轮持续),系统明显受 GC 影响;
    3. M(Metaspace)/CCS 使用率高位(≥85%)且仍上行,存在类加载器相关风险;
    4. 业务侧已出现 Allocation FailureTo-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 -verboseJFR 组合定位暂停来源?请写出关键命令、你关注的 3 类字段/事件(如 TotalTime/ApplicationTime/TimeToSafepointVMOperation 类型、GC/ThreadDump/ClassLoading** 相关事件),以及各自的处置思路。

1) 看 Safepoint 统计(一次或多次)

jcmd <PID> VM.print_safepoint_statistics -verbose

关注 3 类关键信号:

  1. 总停顿与到点耗时

    • Total time for which application threads were stopped(总停顿)
    • Time to safepoint(到达 safepoint 的同步时间)
      判读:到点耗时高 ⇒ 有“坏公民”线程很久不进入安全点(长 JNI/大循环/编译缺少轮询点)。
  2. VMOperation 类型与占比
    表格中 VM Operation(如 G1CollectForAllocationCollectForMetadataAllocationThreadDumpEnable/DisableBiasedLockingRedefineClasses 等)的 Count/Total(ms)/Mean(ms)
    判读:

    • G1*Collect* 为主 ⇒ GC 引发的 STW,需看堆压力/晋升失败。
    • ThreadDump 多 ⇒ 外部频繁打栈造成停顿。
    • RedefineClasses 多 ⇒ APM/热更/Agent 重定义导致。
    • 早期大量 BiasedLock* ⇒ 可考虑 禁用偏向锁(JDK8 可 -XX:-UseBiasedLocking,高版已移除)。
  3. 最大/平均停顿

    • 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 类事件:

  1. GarbageCollection/GC Pause:看暂停时长、原因(Cause)、阶段(如 Evacuation/Remark/Cleanup)
    → 若与 safepoint 长停顿吻合,属 GC 路径
  2. SafepointBegin/End & VMOperation:逐条事件定位 哪类 VM 操作导致停顿、到点耗时操作耗时 分别多大。
  3. 线程/锁/IO 侧佐证JavaMonitorEnter(锁等待)、ThreadParkSocketRead/WriteFileRead/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 步:

  1. 如何获取 core 或触发 jmap/gcore
  2. jhsdb jstack/clhsdb 查看 Java 线程栈与死锁;
  3. 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 比例与晋升阈值、隔离大对象、关闭热点功能等),并写出各手段的适用前提与副作用

  1. 限流/降级/熔断(网关或应用开关)

    • 前提:有网关/开关可控;知道热点接口或租户。
    • 副作用:部分请求失败/排队,SLA 下降;需回滚策略。
    • 怎么做:对高分配率的接口/批量任务限速,关闭“导出/全量查询/大报表”等重操作。
  2. 压缩/清空内存型缓存(本地 Caffeine/Guava、热数据 Map、结果集缓存)

    • 前提:缓存大小/TTL 可在线调整或可安全清空。
    • 副作用:命中率下降、后端压力上升、短时延迟上涨。
    • 怎么做:下调 maximumSize/maximumWeight,主动 invalidateAll(),禁止一次性“大 key 集合”缓存。
  3. 暂停或降速大批处理与后台任务(定时全量跑批、日志聚合、图片/报表生成)

    • 前提:任务可停/可降速,或批量大小可调。
    • 副作用:数据延迟、准实时性下降。
    • 怎么做:减小 batch size/并发度;顺便排查是否存在结果集一次性装内存的代码路径。
  4. 降低日志/埋点/导出采样率,关闭大对象热点功能

    • 前提:有动态配置;明确哪些功能产生大量临时对象(JSON/字符串拼接/压缩)。
    • 副作用:可观测性变差;审计/排障线索减少。
    • 怎么做:降采样比例、关 debug/trace、暂停“导出大 CSV/Excel”。
  5. 应急触发一次 Full GC(仅买时间)

    • 前提:业务可承受一次较长 STW。
    • 副作用:明显停顿;若真泄漏,回收效果有限。
    • 怎么做jcmd <pid> GC.run(或 GC.run_finalization);随后立刻jstat -gcutil 连续观察 + GC.class_histogram 多次对比,决定是否 heap dump

备选(若具备弹性能力):临时水平扩容/分流(加副本数、调度权重),以摊薄分配速率。副作用:资源成本、冷启动抖动。


Q12: 你要做**“堆外/本地内存”**的快速排查。请写出三步闭环:

  1. 如何判断“Java 堆稳定但 RSS 持续上涨”;
  2. VM.native_memory summary(NMT) 定位增长类目(需说明前置条件);
  3. 若未开启 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 内部/编译器分配,异常增长需结合版本/参数排查。

③ 未开 NMT 或证据不足时的无侵入补救思路(任选 2+)

  1. 对比内存映射(定位“谁在长”):
    pmap -x <PID> 周期性采样并 diff;或 smaps_rollup 观察 Rss/Pss/Swap 的时间序列变化,结合 lsof -p <PID> 看是否因映射文件/共享库增长。

  2. JFR 短录制(JDK 11+,低开销):

    jcmd <PID> JFR.start name=nm settings=profile duration=120s filename=/tmp/nm.jfr
    

    在 JMC 中查看 DirectBuffer/Socket/文件 IO/线程创建 等统计与热点;部分版本可见 Direct Memory 用量与分配热点。

  3. JMX BufferPool 观测(直连或临时开启本地管理代理)

    • 检查 java.nio:type=BufferPool,name=directMemoryUsed/TotalCapacity/Count 是否持续上升;
    • 若未开本地代理,可尝试 jcmd <PID> ManagementAgent.start(仅在安全环境使用)。
  4. 线程数与栈大小核对

    ps -L -p <PID> | wc -l      # 线程数
    ulimit -s                    # 每线程栈大小
    

    线程数×栈大小≈潜在 Thread committed,结合业务/池配置降并发或调小线程栈(谨慎)。

  5. 业务侧快速缓解:限流/降级、降低批处理并发、收紧本地缓存容量、减少一次性大对象(如大字节数组)、控制 Netty/Jemalloc Arena 等池化上限。


Q13: 线上发现 DirectByteBuffer 疑似泄漏:请写出你的确认与止血流程(3 步),包括

  1. 如何确认“直接内存”在涨(给出 2 种证据);
  2. 立即止血的 2 条手段(不重启);
  3. 中期修复的 2 个方向(参数/代码)。

① 如何确认“直接内存在涨”(给出 2+ 种证据)

  1. JMX BufferPool(权威直观)

    • 连接 JMX(必要时本机开启:jcmd <pid> ManagementAgent.start_local)。
    • 查看 java.nio:type=BufferPool,name=directMemoryUsed/TotalCapacity/Count 是否随时间单调上升(最好每 20–30s 采样 3–5 次)。
  2. 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
  3. RSS vs Java 堆(系统级交叉验证)

    jstat -gcutil <pid> 2000 10          # 堆与 GCT 斜率很小
    cat /proc/<pid>/status | egrep 'VmRSS|VmSwap'
    
    • Java 堆稳定RSS 持续上涨 → 高度怀疑堆外(直接内存/本地库)。

备注:GC.class_histogram 看不到堆外内存,不要用它来“证明”Direct 泄漏。

② 立即止血(不重启)的 2 条手段

  1. 限流/降级,压制分配速率

    • 对大流量/大报文接口限速、暂停大文件导出/批处理;减少并发连接/入站消息大小(若可动态配置)。
    • 目的:让未被引用的 DirectBuffer 有机会被 GC 回收(Cleaner 触发)。
  2. 低风险补救动作

    • 触发一次 GC(仅买时间):jcmd <pid> GC.run(对 已不可达 的 DirectBuffer 有效;若还被引用则无效)。
    • 如果使用 Netty 且你有管理入口:调用 PooledByteBufAllocator.DEFAULT.trimCurrentThreadCache() 或你们封装的 allocator trim 方法,主动收缩线程本地缓存(版本相关、确保安全调用)。

不能做的:运行期调大/调小 -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 ResourceLeakDetectorADVANCED/PARANOID)定位未释放路径。

代码/业务侧

  • 确保释放:Netty/ByteBuf 路径使用 try { ... } finally { buf.release(); };避免 slice/duplicate 后忘记释放原引用。
  • 降内存形态:大对象优先用 堆内缓冲分块/流式处理(避免一次性 allocateDirect 大片内存);限制聚合器/反序列化的最大消息长度

Q14: 只使用 JFR/JMC 工具链,在线上低开销地捕捉一次 2–3 分钟的证据以定位“CPU 热点 + 内存分配热点”。请写出:

  1. 你会使用的 完整命令序列JFR.start/JFR.check/JFR.dump/JFR.stop,含 settingsdurationfilename 等关键参数);
  2. 导出后的 .jfr 在 JMC 里你优先查看的 5 个视图/事件
  3. 依据这些视图你会做的 两条处置动作(分别针对“计算热点”和“分配过猛”)。

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 个视图/事件

  1. Method profiling / Hot methods(CPU 热点方法、调用栈占比)
  2. Allocation (TLAB / Outside TLAB)(分配速率与分配热点类型/方法)
  3. Garbage Collection(GC 暂停、原因、阶段耗时)
  4. Locks / Java Monitor Blocked(锁竞争、阻塞时间、热点锁)
  5. I/O(Socket/File)(大量读写或慢 I/O 是否与尖峰同窗)

3) 两条处置动作

  • 针对“计算热点(CPU)”

    • 定位前 1–2 个热点方法 → 优化算法/数据结构、下移/缓存结果、拆小批;若是自旋/忙等 → 加退避/超时。
  • 针对“分配过猛(内存)”

    • 对最大分配源做对象重用/缓冲池/流式处理,削峰(限流/降级);必要时扩大新生代或 TLAB(重启后生效),先稳住 GC 压力。

Q15: 线上服务偶发 高 CPU + 频繁 Young GC。请写出你的三段式排查策略

  1. 快速体检:给出 3 条命令看什么(含节奏);
  2. 定位根因:分别从“计算热点”“短命对象暴增”两条支线给出各 2 个证据;
  3. 缓解到修复:各给 2 条(不重启的缓解 / 重启后的修复)。

1) 快速体检(3 条命令 + 节奏 + 看点)

  1. top -H -p <PID>(或 ps -mp <PID> -o THREAD,tid,%cpu -r),连看 10–20s

    • 最高 CPU 的 TID 列表(十进制)。
  2. jstat -gcutil <PID> 1000 20(每 1s × 20 次)

    • ΔGCT/ΔtYGC 增速、E/S0/S1 的“充满→清空”频度、O 是否稳定。
  3. 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 看到分配热点方法/类型。

3) 缓解 → 修复

  • 不重启的缓解(任选 2)

    • 限流/降级 高分配率接口,暂停大报表/批处理,降低并发;缩小本地缓存容量/TTL。
    • 临时证据采集:开启 JFR 2–3 分钟settings=profile),定位热点;必要时 一次性 GCjcmd GC.run)仅用于买时间。
  • 重启后的修复(任选 2)

    • GC/内存参数:合理设定新生代比例(如 G1 的 G1NewSizePercent/G1MaxNewSizePercent)、MaxTenuringThresholdTLAB(结合压测验证)。
    • 代码层:减少临时对象(复用缓冲、对象池、流式处理)、优化热点算法与数据结构;限制序列化/聚合的最大报文/批量

Q16: 当你已确认“频繁 Young GC”且 Old 基本稳定时,请写出一份专治短命对象暴增的 checklist(至少 8 条,覆盖代码与配置两侧),并按“定位 → 改善 → 复验”的顺序编排。

一、定位(低侵入取证)

  1. 时间序列 GC 体检jstat -gcutil <PID> 1000 60 连续 60 秒;看 ΔGCT/ΔtYGC 增速、E/S0/S1 是否“频繁充满→清空”。
  2. 堆概览jcmd <PID> GC.heap_info 快速确认各代占用与阈值(验证 Old 基本稳定、Young 压力大)。
  3. 分配热点(首选)jcmd <PID> JFR.start name=alloc settings=profile duration=120s filename=/tmp/alloc.jfr;在 JMC 里看 Allocation (TLAB/Outside TLAB)类型/方法热点与分配速率。
  4. 类直方图(低频对比)jcmd <PID> GC.class_histogram20–30s 采 3–5 次,对比 Top-K 类型 #instances/#bytes 趋势(注意频率别过高以免频繁 safepoint)。
  5. 业务归因:对齐网关/QPS/接口耗时时序,定位哪个接口/任务触发突增(如大报表、全量导出、批处理)。
  6. 启动参数核对jcmd <PID> VM.flags / VM.command_line 确认 GC 策略、Young 大小(G1 的 G1NewSizePercent/G1MaxNewSizePercent)与 MaxTenuringThreshold 等是否异常。
  7. 日志快速佐证:若可用,查看 GC 日志中 Allocation Failure 频率及 Young GC 原因。
  8. 排除堆外干扰jcmd <PID> VM.native_memory summary(若启用 NMT)或对比 RSS vs 堆/proc/<pid>/status + jstat),确认问题确是堆内短命对象而非堆外。

二、改善(先缓解再修复)

代码层(优先)

  1. 减少临时对象:复用 StringBuilder/byte[]/缓冲区;避免链式 map/filter 产生大量中间对象。
  2. 集合预尺寸:对 ArrayList/HashMap预估容量,避免扩容与 rehash 抖动。
  3. 避免装箱/拆箱:使用原始类型与 TIntArrayList 等(如可用第三方),减少 Integer/Long 短命对象。
  4. JSON/XML 流式化:改用流式解析/序列化,复用 ObjectMapper/Gson 等重对象,避免频繁 new。
  5. 分页/分块:长链路/大结果集改为分页流式处理,禁止一次性 readAll()collect(toList()) 装全量。
  6. 缓存策略:对重复密集计算结果短期缓存(权衡内存),但避免把大对象缓存成“长命”。
  7. 日志与埋点控量:降低高频路径上的字符串拼接/序列化开销(降采样或延迟拼接)。
  8. ThreadLocal 谨慎复用:可复用重对象但注意清理,避免跨请求遗留。

配置/参数(多需重启)
9. 增大新生代:G1 场景调 G1NewSizePercent/G1MaxNewSizePercent(或其他收集器的 -Xmn)以降低 Young GC 频率。
10. 晋升阈值:适度提高 MaxTenuringThreshold,减少早晋升(结合 Survivor 比例);避免老年代被短命对象顶满。
11. TLAB 调优:让热点线程有足够 TLAB(通常交给 JVM 自适应;如需,压测后再调)。
12. 运行期缓解(无需重启):对高分配率接口限流/降级,暂停“导出/全量报表/大批处理”。

三、复验(对比前后效果)

  1. jstat -gcutil 再观测 1–3 分钟YGC 每分钟显著下降;YGCT/GCT 占比下降。
  2. JFR 复录 2 分钟:Allocation 视图中Top 类型/方法的分配速率下降,Outside TLAB 尖峰减少。
  3. 类直方图对比:Top-K 类型的 #instances/#bytes 不再单调上升。
  4. 业务 SLO:P95/P99 延迟与吞吐恢复到阈值内;无 Young GC 风暴告警。

Q17(GC 日志 / 统一日志) 请回答以下三小问(要点式即可):

  1. JDK 8 如何开启最小但有用的 GC 日志?给出常用启动参数各自作用。
  2. JDK 11+ 如何在线上临时开启/导出 GC 统一日志(-Xlog),以及常用的 jcmd VM.log 用法各一条?
  3. 拿到 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+heapsafepointdecorators 常用 time|uptime|level|tags


3) 拿到 GC 日志后,首看 3 个信号判断是否“GC 绑定”

  1. 暂停时长与占比:Young/Full 的单次暂停是否长、短时间内总 GC 时间占比是否高(例如过去 1–5 分钟内 GCT 占比异常)。
  2. 频度与斜率:GC 事件是否越来趟越密(相邻两次间隔缩短、单位时间次数上升)。
  3. 回收后占用趋势老年代/堆在 GC 之后是否明显回落;若 FGC 后仍不降或持续上行,偏向老年代压力/泄漏

Q18(JIT / Code Cache 与容器注意项) 要点式回答:

  1. 如何判断 Code Cache(JIT 代码缓存)逼近上限?(给出 2 条证据与 1 条止血手段)
  2. 容器(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核数>
  • 取证与命名空间

    • 容器内的 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 到可写目录。
  • 时区/时钟与日志路径

    • 统一时区:-Duser.timezone=Asia/Shanghai(或镜像内 TZ);保证 GC/JFR/业务日志时间对齐,便于关联分析。
    • 日志卷挂载与文件轮转在容器内路径保持一致(GC 统一日志 -Xlog:...:filecount,filesize)。

Q19(hs_err_pid 崩溃日志快速判读)

请要点式回答以下三小问:

  1. 拿到 hs_err_pid*.log你第一时间会看哪 6 个关键字段/段落,并各用一句话说明它能帮你判断什么?(例如:# A fatal error has been detected by the Java Runtime Environment, EXCEPTION_ACCESS_VIOLATION/Signal, Problematic frame, ThreadCurrent CompileTaskGC 段、VM ArgumentsOS/CPUloaded librariesNative framesJava framesHeap 摘要等)

  2. 如何初步归因是 JVM Bug / 第三方本地库(JNI) / 代码缓存(Code Cache) / 栈溢出 / 元空间(Metaspace) / 直接内存(Direct) 等?请各给1 条判据(来自日志中的具体线索,如 Problematic frame 指向某个 .soStack: [low, high] guard pagesCodeCache: fullMetaspace 统计、direct buffer 提示等)。

  3. 当场止血的 2 条动作(不重启优先,若必须重启请写出启动参数级的临时止血项各 1 条),并各说明副作用。

1) 拿到 hs_err_pid*.log,先看这 6 个关键段落

  1. 错误头部与信号

    • A fatal error has been detected…EXCEPTION_ACCESS_VIOLATION/SIGSEGV/SIGBUSat pc=…JRE version/VM
    • 用途:确定崩溃类型、PC 地址、JDK 版本与构建(是否命中过已知 Bug)。
  2. Problematic frame

    • 显示出错的库与符号(如 libjvm.solibnetty_transport_native_epoll.solibc.so)。
    • 用途:快速归因 JVM 本身 vs 第三方 JNI/系统库
  3. Current thread / JavaThread(含 Stack: [low, high]

    • 线程名/状态、是否在编译/GC/VM 线程;栈范围与保护页。
    • 用途:判断是业务线程还是 VM 内部线程;栈范围可辅助识别栈溢出
  4. Native frames / Java frames 回溯

    • C/C++ 调用栈 + Java 调用栈(如 Native method、业务方法)。
    • 用途:定位最后的代码路径与是否由某 JNI 调用触发。
  5. VM Arguments

    • 启动参数(GC、ReservedCodeCacheSizeMaxMetaspaceSizeMaxDirectMemorySize 等)。
    • 用途:核对是否存在过小上限/危险开关、便于给临时止血参数。
  6. 内存与代码缓存摘要

    • Heap/Metaspace 概要、CodeCache: 各段使用率与是否 full
    • 用途:识别 内存类问题(Metaspace/堆)与 JIT 代码缓存压力。

备查:OS/CPUloaded librariesCurrent CompileTaskGC 片段也很有用(平台差异、编译中崩溃、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 fullCurrent 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) 当场止血(不重启优先;必须重启的写启动参数)+ 副作用

不重启优先

  1. 流量/功能止血:对可疑接口/任务限流降级、关闭类重定义/热更/频繁打栈(APM/调试)路径。

    • 副作用:SLA 降低/可观测性变差,但能快速降低触发概率。
  2. JIT/CodeCache 缓压

    jcmd <pid> Compiler.directives_add '[{"match":"com.xxx.hot.*","Exclude":true}]'
    jcmd <pid> Compiler.directives_print
    
    • 副作用:被排除方法降级为解释或低级别编译 → 吞吐下降/延迟上升,但可避免 CodeCache 继续被写满或命中编译器崩溃路径。
  3. 堆外/直接内存:限流 +(Netty)trim 线程缓存、减小单请求消息/批量大小;必要时触发一次 jcmd <pid> GC.run 仅回收已不可达的 DirectBuffer。

    • 副作用:吞吐降低;GC.run 可能带来可见 STW
  4. 停止外部高频 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,完成以下要点(要点式即可):

  1. 启动期的取证参数清单(JDK8 vs JDK11+)

    • 至少写出 6 条常用参数,并各用一句话说明作用(例如:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=...-XX:+ExitOnOutOfMemoryError-XX:OnOutOfMemoryError="..."-XX:StartFlightRecording=.../-XX:FlightRecorderOptions=...、统一 GC 日志 -Xlog:gc*,safepoint:... 或 JDK8 的 -Xloggc + 滚动等)。
  2. 事发后 60 秒内的动作序列(不重启)

    • 写出 至少 6 步,包含具体命令/节奏目的(例如:jstat -gcutil <pid> 1000 20 连续 20 秒、jcmd <pid> GC.heap_infojcmd <pid> GC.class_histogram 低频 1–2 次、jcmd <pid> JFR.start ... duration=120stop -H+jcmd Thread.print、网关限流/关闭导出等),并注明哪些动作可能带来 STW、哪些是低侵入
  3. 按 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 风险

  1. 打开统一日志(若未开) 🟢
    jcmd <pid> VM.log what=gc*,safepoint decorators=uptime,level,tags filename=/var/log/app/gc.live.log filecount=5 filesize=20M
  2. 连续 GC 体检(20 秒) 🟢
    jstat -gcutil <pid> 1000 20 → 看 ΔGCT/ΔtYGC/FGC 斜率、O 是否回落。
  3. 堆概览 🟢
    jcmd <pid> GC.heap_info → 各代 used/committed、算法与阈值。
  4. 类直方图(低频 1–2 次,间隔 20–30s) 🟡
    jcmd <pid> GC.class_histogram > /tmp/histo1.txtsleep 20;再采一次对比 Top-K #bytes/#instances 趋势。
  5. 启动 JFR 取证(120 秒) 🟢
    jcmd <pid> JFR.start name=oomtriage settings=profile duration=120s filename=/tmp/oomtriage.jfr → 捕捉分配热点/锁/IO。
  6. CPU/线程侧交叉验证(必要时) 🟡
    top -H -p <pid> → 取 TID;jcmd <pid> Thread.print > /tmp/th.txt → 排除计算/锁导致的内存放大。
  7. 业务侧止血(立刻执行) 🟢
    限流/降级/暂停导出与批处理、缩小分页;下调本地缓存上限;避免一次性加载全量数据。
  8. (极端)买时间动作 🔴
    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())。副作用:需要变更调用方逻辑
  • 重启后修复

    • 合理设 -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)

  1. 场景选型:在什么情况下优先选择 G1,什么情况下优先选择 ZGC?请给出至少 6 条决策因素(如堆大小/暂停目标/吞吐 vs 延迟/JDK 版本与成熟度/本地内存占用/容器化/对象寿命分布/可观测性与调参复杂度等),并分别简述理由。

  2. G1 基线:给出一套你在生产容器环境常用的 G1 启动参数基线(写成一行或多行均可),并逐项用短语说明作用(例如堆与百分比:-Xms/-Xmx-XX:InitialRAMPercentage/MaxRAMPercentage、暂停目标 -XX:MaxGCPauseMillis、新生代上限下限 G1NewSizePercent/G1MaxNewSizePercent、触发并发标记 InitiatingHeapOccupancyPercent、保留比例 G1ReservePercent、并行/并发线程、AlwaysPreTouch、统一 GC 日志滚动等)。

  3. ZGC 基线:给出一套 ZGC 启动参数基线(JDK 17+ 场景优先),同样逐项用短语说明作用(如 -XX:+UseZGCSoftMaxHeapSizeZUncommitDelayZCollectionIntervalConcGCThreads、统一日志、容器内存百分比参数等),并说明 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),对抖动更敏感的业务优先。
  • 吞吐 vs 延迟权衡

    • G1:总体吞吐通常更好(更少并发搬迁开销)。
    • ZGC:为低延迟付出一点吞吐与 CPU 并发开销。
  • JDK 版本与成熟度

    • G1:JDK8/11 起长期打磨、参数/实践多。
    • ZGC:JDK17+ 非常成熟(生产可用),JDK21+ 支持代际 ZGC(更优分代分配/回收)。
  • 碎片与压实能力

    • G1:分区+按需混合回收,停顿中压实;碎片控制尚可。
    • ZGC并发移动对象,碎片控制最佳,长时间运行更平稳。
  • 内存弹性 / 释放空闲

    • G1:可回收 free region,但对RSS 回落不如 ZGC 灵活。
    • ZGCSoftMaxHeapSize + 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 项:

  1. 暂停分布:JFR/-XlogGC Pause P99 ≤ 10–20 ms(按业务 SLA 设阈值),且尾部稳定无长尾尖峰。
  2. 分配与并发周期:Allocation rate 在峰值下 无明显 pacing/Allocation Stall;单次并发周期耗时稳定(秒级)且不频繁回退到 Full。
  3. 资源占用:GC 线程 CPU 占比可控(常 <5–10%),RSS 随闲忙可回落(验证 SoftMaxHeapSizeZUncommit 生效)。

Q22题(线程 Dump 状态判读与处置)

  1. RUNNABLEBLOCKEDWAITING/TIMED_WAITINGPARKEDNATIVE 五类线程在 Thread.print/jstack 中常见的栈顶方法特征各 2 个;
  2. 各状态对应的可能根因快速处置动作(各给 1–2 条);
  3. 如何把 top -H十进制 TID 映射到 Thread.printnid=0x..(写出关键命令)。

1) 常见线程状态的“栈顶方法特征”(各 2 个)

  • RUNNABLE

    • 纯计算热点:java.util.*(排序/流式聚合/正则/JSON)、BigDecimal/压缩/加密等方法反复出现。
    • 伪 RUNNABLE 的阻塞 I/O:java.net.SocketInputStream.socketRead0sun.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.parkLockSupport.park/parkNanos)。
    • 线程池/队列空闲:java.util.concurrent.ForkJoinPool.awaitWorkjava.util.concurrent.SynchronousQueue$TransferStack.transfer
  • NATIVE(本地栈热点/JNI)

    • I/O 原生调用:read0/write0/poll/epollWait/accept0
    • JNI/库调用:com.xxx.NativeLib.doCall(Native Method)Unsafe.copyMemory 等。

2) 可能根因与快速处置

  • RUNNABLE

    • 根因:CPU 计算热点/死循环,或 I/O 阻塞被标成 RUNNABLE。
    • 处置:用 JFR 看 Hot Methods/Allocation;优化算法或加退避;若是下游 I/O 慢→校验超时/重试/限流。
  • BLOCKED

    • 根因:锁竞争/锁顺序反转。
    • 处置:根据 waiting to lockowner 对齐热点锁,缩小临界区、换无锁/分段锁;必要时先降并发。
  • WAITING / TIMED_WAITING

    • 根因:条件等待/超时等待、线程间通信不匹配。
    • 处置:核对通知/超时/队列长度,避免无限等待;检查上游是否“喂不饱”导致饥饿。
  • PARKED

    • 根因:线程池空闲/阻塞在工作队列、限流/背压。
    • 处置:核对任务提交/取出速率、队列容量;避免过度并发造成反压失衡。
  • NATIVE

    • 根因:JNI/文件/网络/磁盘原生调用慢或挂起。
    • 处置:加超时与熔断;定位具体 .so/系统瓶颈(磁盘/网络),必要时降载。

3) 把 top -H 的十进制 TID 映射到 Thread.printnid=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 精确分析:最小流程)

  1. 何时抓 dump:给出 3 个触发阈值/时机(如 FGC 后 Old 不回落、Top-K 类直方图 3 次单调上升、Metaspace 高位等),以及为何选低峰
  2. 如何抓 dump:写出命令(优先 jcmd <pid> GC.heap_dump filename=...),并说明磁盘/权限/暂停风险注意事项。
  3. 用什么看:列出 3 个核心视图与提问(如 Dominator Tree 定位最大保留、Leak Suspects ReportPath to GC Roots 的强/软引用链),以及各自产出什么结论。
  4. 从证据到修复:给出 3 条典型修复动作(如移除静态集合引用、修 ThreadLocal 泄漏、缩小缓存/加 TTL、流式化/分页等),并说明如何复验(哪些指标应改善)。

1) 何时抓 dump(触发阈值/时机 ×3+;为何选低峰)

  • 老年代不回落FGC 连续发生且 O(Old)在 FGC 后回落 < 5–10%,持续 2–3 个周期
  • 直方图单调上升jcmd GC.class_histogram 每 20–30s3 次同一 Top-K 类#bytes/#instances 持续上行(尤其跨 FGC 后仍上行)。
  • Metaspace 高位M/CCS 使用率 ≥85% 且仍上行;或 GC 日志出现 Metadata GC Threshold
  • (可选)业务异常Allocation FailureTo-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 会进 Safepointjmap -F 风险更大,慎用。
  • 完成后gzip -9 app-*.hprof 压缩、校验 md5,避免跨环境泄漏隐私数据。

3) 用什么看(核心视图 ×3 与提问)

  • Dominator Tree(支配树 / Retained Size)

    • :谁在“最大保留”内存?哪些对象/容器(如 byte[]char[]HashMapConcurrentHashMap)支配了大量内存?
    • 产出:定位内存根因集合与其拥有者(类、单例、缓存、会话等)。
  • Path to GC Roots(到根路径,排除软/弱)

    • :这些大对象为何不可回收?强引用链是不是静态字段线程(含 ThreadLocal)ClassLoader
    • 产出:给出精确引用链,能落到代码处置点。
  • Leak Suspects / Histogram(嫌疑报告 / 直方图)

    • :是否存在“疑似泄漏分量”;哪几类的 #bytes/#instances 最大?
    • 产出:快速锁定嫌疑类型实例分布;用 Histogram 验证堆内“谁最大”。

常用工具:Eclipse MATJMC Heap AnalyzerYourKit。MAT 中可用 OQL 进一步过滤(比如某 Map 的 key 前缀)。

4) 从证据到修复(典型动作 ×3;如何复验)

  • 移除长寿命强引用:清理/缩小静态集合/单例缓存;引入 Caffeine 限额/TTL/最大权重。

  • 修 ThreadLocal 泄漏:请求完成后 finally { tl.remove(); };避免在线程池中遗留大对象。

  • 流式化 / 分页:避免一次性加载/拼接巨量数据;改为分页/分块/流式处理,复用缓冲。

  • (可选)类加载器泄漏:确保插件/热更 ClassLoader close() 并断开静态回指。

  • 复验

    • jstat -gcutil <PID> 1000 120YGC 频度下降GCT 斜率变小、O 保持稳定/更低。
    • 再抓一次 Histogram 对比:嫌疑类型的 #bytes/#instances 不再单调上升
    • 业务 SLO:P95/P99 延迟恢复,GC 日志暂停分布改善。

Q24(ClassLoader 泄漏定位与修复)

  1. 线上判定 ClassLoader 泄漏 的 3 个信号(含一个来自 堆分析、一个来自 NMT/内存、一个来自 运行现象)。
  2. MAT/JMC 从 dump 到“定位哪个 ClassLoader 保留了哪些类/对象”的 3 步法。
  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 ThresholdMetaspace 使用率高位并继续爬升,最终可能 OutOfMemoryError: Metaspace;多次热加载/灰度后常复现。

2) 用 MAT/JMC 从 dump 到“哪个 ClassLoader 在保留谁”的 3 步

  1. 锁定 Loader:MAT → Histogram 搜索 *ClassLoader,排序看 Shallow/Retained Size 最大的 Loader;或用 Class Loader Explorer 视图(若有)。
  2. 看支配关系:对该 Loader 点 “Merge Shortest Paths to GC Roots (exclude soft/weak)” 或直接在 Dominator Tree 中展开,确定是 静态单例/线程/ThreadLocal 等在保留它。
  3. 落到类型与实例:右键 “List Objects → with incoming references” 找到导致保留的具体字段/集合;并在 Histogram 中按 Loader 过滤,看由它加载的重复类/大数组/缓存等热点类型。

备选:在线快速统计

jcmd <pid> VM.classloader_stats       # 各类加载器已加载类计数/占用(不同 JDK 版本命令名可能略有差异)
jcmd <pid> VM.system_properties | grep -i metaspace

3) 代码与配置侧的 4 条修复建议(含副作用/权衡)

  1. 关闭并释放可卸载 Loader:对自建/插件化的 URLClassLoader 调用 close(),停止由该 Loader 创建的后台线程,清理 ThreadLocal
    副作用:动态加载功能/热更时机需重新设计,关闭过早会影响运行中的模块。
  2. 断开静态回指:避免从父加载器的单例/缓存持有子加载器对象或其类实例;将可变缓存下沉到子加载器私有范围;
    副作用:需要梳理全局单例,拆分缓存作用域。
  3. 控制动态生成类/代理(CGLIB/ASM/Javassist/脚本引擎)数量与生命周期,复用 ClassLoader 或在卸载前批量清理;
    副作用:灵活性下降,可能影响热扩展能力。
  4. 规避 ThreadLocal 泄漏:业务线程池中使用的 ThreadLocal 在请求完成后统一 remove();避免将应用类实例放入父加载器可见的单例/ThreadLocal;
    副作用:需要编码规范与审计,改造成本。

运行参数层面(重启生效):增大 -XX:MaxMetaspaceSize 只能买时间,不能治本;务必配合修复引用链。


Q25(统一日志进阶:如何量化“是否 GC 绑定”) 给出一段可操作的量化判据与命令组合:

  1. 你如何用 -Xlog:gc*,safepoint(或 JDK8 GC 日志)5 分钟窗口内量化“GC 时间占比 ≥ 30% 即判定 GC 绑定”?写出日志开启方式一条 awk/grep 统计思路
  2. 若不是 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 瓶颈
  • 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 长停顿量化)

  1. jcmd VM.print_safepoint_statistics -verbose-Xlog:safepoint,在 5–10 分钟窗口内量化 “Time to safepoint 占比”“VM operation 耗时 Top-N” 的方法(给出命令与一条 awk/grep 统计思路);
  2. 到点耗时占比高 时你会优先怀疑哪三类根因,并各给一条快速验证思路;
  3. 给出两条当场止血与两条中期修复(分别说明副作用)。

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 timetime to safepoint,上面用正则兼容。


2) 若 TtS 占比高,优先怀疑的三类根因 & 快速验证

  1. JNI/系统调用卡住(线程长时间在 native,不进 safepoint)

    • 验证jcmd Thread.print 观察大量线程栈顶为 socketRead0/read/write/epollWait/accept0 或第三方 Native Method;必要时 top -H 取 TID → strace -fp <tid> 看到系统调用久返。
  2. 长计算循环缺轮询点(热循环几乎不触发 safepoint poll)

    • 验证:多次 Thread.print 同一批 RUNNABLE 业务栈稳定不变;JFR Hot Methods 占比极高而 ΔGCT/Δt≈0(非 GC)。
  3. GC Locker/JNI Critical 区域(如 GetPrimitiveArrayCritical 持有过久)

    • 验证:JFR/栈里频见 *Critical 路径或相关 native;(可选)打开 -Xlog:gc+locker=info 看是否频繁 “GC locker is occupied”

其他:频繁的 ThreadDump/RedefineClasses 会拉高VM operation time(而非 TtS 本身),但同样会扩大总停顿,需一并留意。


3) 当场止血 ×2 & 中期修复 ×2(含副作用)

当场止血(不重启优先)

  • 限流/降级/暂停大任务:压低制造“坏公民线程”的入口(大报表、压缩/加密、重 I/O/大 JSON)。
    副作用:吞吐/功能下降,但能迅速降低 TtS。
  • 停止外部重操作:暂停高频 jstack/kill -3/APM ThreadDump、热更/重定义(RedefineClasses)。
    副作用:可观测性与调试手段受限。

中期修复(多需重启或改代码)

  • 缩短 native/计算临界段:把长 JNI/系统调用切小、加超时与中断;对热循环引入可中断点/轮询点或优化算法。
    副作用:代码改造成本;可能影响性能路径,需要压测。
  • 参数层面缓解 TtS 极端值:重启加
    -XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=2000(2s)
    作为“安全阀”限制最大 TtS。
    副作用:轻微额外开销;治标不治本,仍需修正根因。

Q27(NMT 原生内存跟踪 · 堆外内存快速定位)

  1. 前置条件与开销:开启 NMT (Native Memory Tracking) 需要哪些启动参数?它的典型开销多大、什么时候不适合长期开启?

  2. 三步操作法:写出用 jcmd VM.native_memory 进行baseline → 等待 → summary.diff 的三条命令,并说明每一步你在看什么字段(至少列出 Thread/Class(Code)/Internal(Other)/GC 其中三类)。

  3. 三种典型场景的判读与动作(各给 1 条判据 + 1 条当场动作):

    • 线程栈上涨(Thread 类目)
    • Metaspace 上涨(Class 类目)
    • 直接内存/JNI 涨(Internal/Other 类目)
  4. 如果启动时没开 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.diffThread 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=directMemoryUsed/Count 是否上行
    • ps -L -p <pid> | wc -l 侧证线程数;
    • -Xlog:codecache=infojcmd <pid> VM.log what=codecache=info 看 Code Cache 使用率。
      局限:JMX 未必可用;跨组件来源仍需结合日志/JFR判断。

Q28(JFR 预设与低开销取证 · 配置与验收)

  1. JFR 预设差异:说明 settings=default / profile / continuous 三者的覆盖范围与开销差异,各自适用场景各举 1 条。

  2. 两套落地“配方”

    • 短时取证(2–3 分钟,CPU+内存分配热点并重):写出完整命令(JFR.start/JFR.dump/JFR.check/JFR.stop)与关键参数(如 filenamestackdepthsamplethreads),并说明预期开销。
    • 低扰动巡检(30 分钟以上):写出一套更稳的配置(例如 settings=default、较小 stackdepth、周期性 dump),并说明如何限速磁盘占用
  3. 上线验收:列出 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 rateJava Monitor Blocked P99 无显著上扬(与基线相比变化 <10%)。

Q29(TLAB / Outside TLAB(TLAB 外分配) 分配监控与优化)

  1. 解释 TLABOutside TLAB 的区别;用 JFR-Xlog:gc+tlab=debug(JDK11+) 怎么观察它们的分配量?
  2. 在“频繁 Young GC”场景下,Outside TLAB 比例过高说明了什么?给出 3 条可能原因 与相应 优化动作(如对象过大、TLAB 太小、跨线程发布等)。
  3. 写出一条最小可行命令组合(JFR 或日志)与判定阈值,用来在 2–3 分钟内快速判断“是否需要针对 TLAB 做调优或代码侧整改”。

1) 概念与观测

  • TLAB 内分配:每个线程在 Eden 拿到一块 Thread-Local Allocation Buffer,走“指针递增”快速分配,几乎无锁。

  • TLAB 外分配(Outside TLAB):没有在本线程的 TLAB 中完成的分配,走 全局慢路径(直接 Eden/老年代/巨大对象路径)。常见于对象太大TLAB 剩余不足

  • 如何观察(任选其一):

    • JFR(推荐)Object Allocation in TLABObject 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 判读)

  1. 用哪些手段查看对象年龄分布?分别写出 JDK 8 与 JDK 11+ 的最小命令(如 -XX:+PrintTenuringDistribution-Xlog:gc+age=trace)。
  2. 看到“早晋升(年龄很小就进老年代)”与“Survivor 撑爆”各说明什么问题?各给 2 条可能成因与 1 条调参/代码动作。
  3. 给出一条最小体检脚本(3–5 行),在 1 分钟内每 5 秒采一次年龄分布,并说明你据此如何判断是否需要调 MaxTenuringThresholdSurvivor 比例

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 条)

    1. Survivor 太小,无法容纳当次存活对象;
    2. MaxTenuringThreshold 偏低 或对象存活率偏高(短时间内多次被拷贝)。
  • 动作(择一落地)

    • 增大 Survivor/新生代(G1:调 G1NewSizePercent/G1MaxNewSizePercent;并确保 Survivor 容量充足),或适度提高 MaxTenuringThreshold(重启生效,压测评估)。
B. “Survivor 撑爆/溢出”(to-space 不够)
  • 含义:日志显示 survivor 溢出/desired survivor size 远小于实际存活,伴随 new threshold 下降;有时伴 promotion failure 警告。

  • 可能原因(举 2 条)

    1. 瞬时存活对象过多(例如批量组装、串联中间集合);
    2. 对象过大或 Outside TLAB 比例高,加剧 Eden→Survivor 压力。
  • 动作(择一落地)

    • 扩大新生代或 Survivor 占比(G1 可调 G1MaxNewSizePercent;并关注 TargetSurvivorRatio 自适应结果),同时代码侧降存活峰值(分页/流式、预尺寸、复用缓冲)。

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)与其大多数调参、G1HeapRegionSizeMaxMetaspaceSizeReservedCodeCacheSizeMaxDirectMemorySizeNativeMemoryTracking 开关等。

三、常用“可执行动作”但不是“改参数”

  • 触发 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 -verboseVM.native_memory summary(NMT 需启动开启)。

Q32(统一日志选择器速查 & 提取范式)

  1. 选择器速查:写出 至少 6 组常用 -Xlog 选择器组合(含等级或分类),并各用半句说明用途(例:gc+phases=debuggc+heap=infogc+age=tracegc*=infosafepoint=infocodecache=infogc+tlab=debug 等)。

  2. 运行期开启/关闭范式:给出 2 条你线上常用的 jcmd VM.log 命令(一个落到文件滚动、一个打到 stdout 即席观察),以及如何关闭

  3. 5 分钟窗口聚合:给出 1 段 awk/grep 思路(或伪代码),从带 uptime 装饰器的 GC 统一日志里计算最近 5 分钟内:

    • a) 暂停总时长事件次数(Young/Full 可分开或合并);
    • b) GC 时间占比(暂停总时长 / 300s);
    • c) 若开启了 gc+age=trace,统计最近窗口里出现的 new threshold 最小值
  4. 阈值判定(给出数字):写出你判定“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 msYGC 次数 ≥ 30/5 分钟
  • 当场动作(任选两条)

    • 限流/暂停重任务(报表/导出/大批处理),降低分配速率;
    • 一次性 jcmd <pid> GC.run 在低峰执行,仅作买时间;
    • 压缩/清空本地缓存、降低日志/序列化开销;
    • 短录 JFR 180s 捕捉分配与 GC 证据(settings=profile)。

Q33(JIT 快速绕路:Compiler.directives 应急)

  1. 给出一套在线绕开热点方法的命令:添加、查看、清除 Compiler.directives;并说明“绕开”的直接效果是什么(对该方法的编译/执行形态)。
  2. 何时适合用它做应急止血?列出 2 个触发条件(如 code cache 逼近、JIT 疑似崩溃路径)。
  3. 给出 回滚与验证 的做法(包括如何确认 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_errProblematic 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 体检与编译队列)

  1. 写出你在线上**判断是否出现“编译风暴/队列堆积/CodeCache 压力”**的最小命令组合(≥3 条),并说明各自看点(如 Compiler.queue-Xlog:compilation-Xlog:codecache=info)。
  2. 若确认队列堆积且 CodeCache 使用率>90%,给出 两条不重启止血两条重启后修复(各注明副作用)。
  3. 用一句话说明为何“编译风暴”会放大请求延迟,即使编译线程是后台线程。

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 频繁,老年代稳定”。

  1. 写出一段 3–5 行命令脚本,在 120 秒 内同时采集:jstat -gcutil、两次 Thread.print、以及一段 JFR 180s(即可刻启动)。
  2. 写出你在 60 秒 内如何判定“计算热点 vs 短命对象暴增”(各给 2 个证据点)。
  3. 若最终判定是“短命对象暴增”,给出 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 -gcutilYGC 快速递增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 撑爆”)

  1. 给出运行期开启年龄分布日志的命令(JDK 11+,jcmd VM.log),并说明要加的 decorators
  2. 写一段 awk/grep 思路:从带 uptime 的统一日志中,在最近 5 分钟窗口统计 new threshold 的最小值,并判断是否出现“早晋升”(给出你的阈值)。
  3. 若判定“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 外分配”)

  1. 给出一段 3–5 行命令,120 秒内同时采集:jstat -gcutilVM.log gc+age=trace、以及一段 JFR 180s
  2. 写出你据此在 60 秒内判定“是否早晋升”的 2 个信号(分别来自年龄日志与 jstat)。
  3. 若同时观察到 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 条)

    1. 分块/流式处理:避免一次性分配超大 byte[]/char[]/String;复用缓冲区(例如复用 StringBuilder/byte[])。
    2. 减少跨线程发布:尽量在实际消费的线程内创建对象,避免在提交到线程池前构造大对象导致 TLAB 外分配与逃逸。
  • 参数侧(1 条,重启项)

    • 适度增大新生代/Survivor 容量(G1:-XX:G1NewSizePercent / -XX:G1MaxNewSizePercent 上调;必要时评估 -XX:MinTLABSize),在压测验证后上线。权衡:堆布局变化,吞吐/停顿曲线需复验。

http://www.dtcms.com/a/347178.html

相关文章:

  • 离线优先与冲突解决:ABP vNext + PWA 的边缘同步
  • SQL Server更改日志模式:操作指南与最佳实践!
  • 使用 Certbot 申请 Apache 证书配置棘手问题
  • UAD详解
  • 分库分表系列-核心内容
  • 知识蒸馏 Knowledge Distillation 概率链式法则(Probability Chain Rule)
  • Class42时序模型
  • 深度学习开篇
  • 【通俗易懂】TypeScript 的类型守卫 (Type Guards)作用理解
  • iperf2 vs iperf3:UDP 发包逻辑差异与常见问题
  • [新启航]白光干涉仪与激光干涉仪的区别及应用解析
  • ubuntu 新登录修改root密码
  • 【攻防世界】Web_php_include
  • 力扣热题之动态规划
  • CryptSIPVerifyIndirectData函数分析
  • 鸿蒙开发进阶(HarmonyOS)
  • STM32 外设驱动模块八:红外反射式光电模块
  • 【大语言模型 15】因果掩码与注意力掩码实现:深度学习中的信息流控制艺术
  • 2-5.Python 编码基础 - 键盘输入
  • 2025钉钉十周年新品发布会,新品 “蕨”命名,到底是什么?
  • vue3 - 组件间的传值
  • nodejs和vue安装步骤记录
  • 【Golang】有关任务窃取调度器和抢占式调度器的笔记
  • 机器人 - 无人机基础(5) - 飞控中的传感器(ing)
  • 【大语言模型 16】Transformer三种架构深度对比:选择最适合你的模型架构
  • 云原生俱乐部-k8s知识点归纳(8)
  • 资深产品经理个人能力提升方向:如何系统化进阶并考取高价值证书?
  • 资深产品经理个人能力提升方向:如何系统化进阶与考证规划
  • 可视化-模块1-HTML-02
  • Node.js特训专栏-实战进阶:23. CI/CD流程搭建