QUICKREF · JVM · GC · DIAGNOSTICS
JVM、GC 与诊断工具
JDK 21 · 速查主题 11 / 12 · 预计 30 分钟
一句话JDK 21 的运行时主线是「更低停顿 + 更强诊断」:G1 仍是默认 GC,分代 ZGC 转正;JFR 与 jcmd 覆盖了从 GC、线程到虚拟线程的主要排障路径。先学会拿数据,再谈调优。
学完你要能自己回答
- G1 和 ZGC 的目标差异是什么?什么时候才应该选 ZGC?
- JDK 21 的 class 文件版本是多少?如何用代码读取运行时版本?
- 常见 OOM 类型分别意味着什么?第一动作应该做什么?
运行时与版本01
JDK 21 生成的 class 文件主版本号是 65(Java 8 是 52,每六个月 +1)。跨版本运行时,低版本 JVM 无法加载高版本 class。
Runtime.Version version = Runtime.version();
System.out.println(version); // 21.0.12+8-LTS
System.out.println(version.feature()); // 21
System.out.println(System.getProperty("java.version"));
System.out.println(System.getProperty("java.vendor"));
System.out.println(System.getProperty("java.vm.name"));
System.out.println(System.getProperty("os.arch"));
# 查看 class 文件版本
javap -verbose MyClass.class | grep "major version"
# JDK 21 -> major version: 65
# JVM 参数与默认值
java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseZGC|MaxHeapSize"
从 JDK 10 起 JVM 能感知容器 CPU / 内存限制;容器里不要只依赖
-Xmx,也要关注 MaxRAMPercentage 与 ActiveProcessorCount。内存结构:堆、元空间、直接内存02
| 区域 | 存放 | 常见参数 | 溢出 |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | -Xms / -Xmx | Java heap space |
| 元空间(Metaspace) | 类元数据、方法字节码 | -XX:MaxMetaspaceSize | Metaspace |
| 压缩类空间 | 类指针压缩结构 | -XX:CompressedClassSpaceSize | Compressed class space |
| 直接内存 | NIO DirectByteBuffer | -XX:MaxDirectMemorySize | Direct buffer memory |
| 线程栈 | 每个平台线程的栈 | -Xss | unable to create native thread |
| 代码缓存 | JIT 生成的机器码 | -XX:ReservedCodeCacheSize | CodeCache is full |
JDK 21 的堆仍然分代(年轻代 / 老年代),G1 按 Region 管理。对象优先在 Eden 分配,经历多次 GC 后晋升老年代;大对象可能直接进入 Humongous Region。
# 查看堆与 GC 概况
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram | head -30
jstat -gcutil <pid> 1000 10
# OOM 时自动 dump
java -Xmx512m -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap.hprof -jar app.jar
jmap -histo:live 会触发一次 Full GC,生产环境慎用;优先用 jcmd GC.class_histogram 或 JFR 的堆采样。GC 选型:G1 默认,ZGC 低延迟03
JDK 21 默认 G1。ZGC 在 15 转正,21 引入分代 ZGC(JEP 439),需要在 21 上显式开启分代模式;到 23 分代成为默认,24 移除非分代模式。
| GC | 目标 | 停顿 | 适用 |
|---|---|---|---|
| Serial | 小堆、单核 | 较长 | 客户端、小工具 |
| Parallel | 吞吐优先 | 较长 | 批处理、离线计算 |
| G1 | 平衡吞吐与停顿 | 可预测(默认目标 200ms) | 大多数服务端默认 |
| ZGC | 极低停顿 | 通常 < 1ms(与堆大小弱相关) | 大堆、延迟敏感 |
| Shenandoah | 低停顿 | 较低 | 低延迟,另一条实现路线 |
# G1(JDK 21 默认)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# ZGC 分代模式(JDK 21 需显式开启)
java -XX:+UseZGC -XX:+ZGenerational -Xmx16g -jar app.jar
# 查看 GC 日志
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
- 堆越大,ZGC 的相对优势越明显;小堆用 G1 通常足够。
- ZGC 的吞吐与内存开销通常高于 G1,不要只看停顿。
MaxGCPauseMillis是目标不是保证;G1 会通过调整年轻代大小逼近目标。- 不要同时开启多个 GC 参数;
-XX:+UseZGC与-XX:+UseG1GC互斥。 - 升级到 JDK 23+ 后,ZGC 分代是默认,不需要再写
ZGenerational。
调优顺序:先确认瓶颈是 GC(日志/停顿),再调堆与 GC;多数「GC 问题」其实是内存泄漏或分配速率过高,换 GC 只是掩盖症状。
jcmd / JFR / 线程转储04
JDK 21 的诊断入口首选 jcmd:它把 JFR、线程、GC、VM 信息整合到一个工具里,不需要额外安装。JFR 是低开销的持续记录器,适合生产环境。
| 命令 | 用途 |
|---|---|
jcmd <pid> VM.version | 版本与 JVM 信息 |
jcmd <pid> VM.flags | 当前生效参数 |
jcmd <pid> GC.heap_info | 堆使用与 GC 概况 |
jcmd <pid> Thread.print | 线程转储(平台线程) |
jcmd <pid> Thread.dump_to_file -format=json | JSON 线程转储(含虚拟线程) |
jcmd <pid> JFR.start duration=60s filename=app.jfr | 启动 JFR 记录 |
jcmd <pid> JFR.dump filename=app.jfr | 导出运行中的 JFR |
jfr summary app.jfr | 查看记录概况 |
jfr print --events jdk.GCPhasePause app.jfr | 打印指定事件 |
# 发现 Java 进程
jps -l
# 采集 60 秒 JFR(低开销)
jcmd <pid> JFR.start duration=60s filename=/tmp/app.jfr
# 线程转储
jcmd <pid> Thread.print > /tmp/threads.txt
# 查看死锁
jcmd <pid> Thread.print -l | grep -A 20 "Found one Java-level deadlock"
JFR 的默认配置开销很低,适合长期开启。线上排障时先看 jdk.GCPhasePause、jdk.ExecutionSample、jdk.VirtualThreadPinned、jdk.JavaMonitorEnter 等事件。
OOM 类型与第一动作05
| 错误 | 含义 | 第一动作 |
|---|---|---|
Java heap space | 堆内存不足 | dump 堆,查大对象 / 泄漏;再考虑调大 -Xmx |
Metaspace | 类元数据耗尽 | 查类加载器泄漏、动态生成类;限制 MaxMetaspaceSize |
Direct buffer memory | 直接内存耗尽 | 查 Netty / NIO 泄漏;设置 MaxDirectMemorySize |
unable to create native thread | 平台线程创建失败 | 降低线程数 / 栈大小;考虑虚拟线程 |
GC overhead limit exceeded | GC 占用过高且回收很少 | 几乎总是堆太小或泄漏,先 dump |
Requested array size exceeds VM limit | 数组长度超限 | 检查业务计算是否溢出 |
Compressed class space | 压缩类空间耗尽 | 查类加载泄漏,调整空间参数 |
# 生产环境建议常开
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/dumps \
-Xlog:gc*:file=/data/logs/gc.log:time,uptime:filecount=5,filesize=50M \
-jar app.jar
- 先保现场:堆 dump、GC 日志、线程转储,再重启。
- 不要在没有 dump 的情况下反复重启;问题会重复出现。
MetaspaceOOM 常见于热部署、动态代理、脚本引擎反复生成类。Direct buffer memory不占堆,堆 dump 看不出来;需要 NIO 泄漏检测或 JFR。
JIT、启动与性能06
HotSpot 使用分层编译:C1 快速编译,C2 深度优化。方法需要「热」到阈值才会被 C2 编译,因此短命进程、微基准测试很容易测到解释执行或 C1 代码。
- 性能测试必须预热,或使用 JMH 这类框架;不要用
System.currentTimeMillis循环测单次调用。 - 逃逸分析可以把未逃逸对象标量替换,减少分配;但这依赖 JIT,不要据此写「聪明」代码。
-Xint只用于调试,不要用于生产。- 代码缓存满会导致退化为解释执行,日志里会出现 CodeCache 相关警告。
- 启动优化可考虑 AppCDS(JDK 10+)、AOT 缓存(JDK 24+ 逐步成熟)。
# 查看 JIT 与代码缓存
jcmd <pid> Compiler.codecache
jcmd <pid> Compiler.queue
# 统一日志查看类加载 / GC
java -Xlog:class+load=info,gc*=info -jar app.jar
性能问题先分清是 CPU、内存、GC、锁还是 I/O。JFR 的 ExecutionSample 看 CPU 热点,JavaMonitorEnter 看锁竞争,GCPhasePause 看停顿;不要一上来就调 JVM 参数。
虚拟线程的运行时观测07
虚拟线程不是 OS 线程,传统 jstack 只能看到载体线程。JDK 21 提供了 JSON 线程转储与 JFR 事件,专门用于观察虚拟线程。
# JSON 转储,包含虚拟线程
jcmd <pid> Thread.dump_to_file -format=json /tmp/threads.json
# JDK 21 检测 pinning
java -Djdk.tracePinnedThreads=full -jar app.jar
# JFR 事件
jcmd <pid> JFR.start duration=60s filename=/tmp/vt.jfr
jfr print --events jdk.VirtualThreadStart,jdk.VirtualThreadEnd,jdk.VirtualThreadPinned /tmp/vt.jfr
jdk.VirtualThreadPinned默认记录超过 20ms 的钉住事件。- 虚拟线程数量不是问题本身;要关注载体线程利用率与下游瓶颈。
- JDK 24+ 已消除 synchronized 导致的 pinning,升级后相关告警会消失。
- JFR 的
jdk.ThreadStart与jdk.VirtualThreadStart是不同事件,不要混淆。
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| 分代 ZGC | 21 正式(需开启) | JEP 439;JDK 23 成为默认,24 移除非分代模式。 |
| 默认 GC | G1 | JDK 9 起 G1 为默认;JDK 27 起所有环境默认 G1。 |
| 紧凑对象头 | 24 实验 / 25 正式 | JDK 21 不可用。 |
| JFR 开源 | 11 正式 | JDK 8 需商业许可,11 起开源。 |
| 容器感知 | 10 起 | JDK 8u191+ 也有部分支持。 |
| 动态加载 agent 警告 | 21 警告 | JEP 451。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. G1 和 ZGC 的核心目标差异是什么?
G1 追求吞吐与可预测停顿的平衡,通过 Region 和停顿目标调优,适合大多数服务;ZGC 追求极低停顿(通常亚毫秒级),通过并发标记/转移把停顿与堆大小解耦,适合大堆、延迟敏感场景,但通常有更高的内存与吞吐开销。
02. JDK 21 的 class 文件版本是多少?
主版本号 65。Java 8 是 52,之后每个 feature release 加 1,因此 21 对应 65,25 对应 69。
03. 遇到 Metaspace OOM 应该先查什么?
先查类加载器泄漏与动态类生成:热部署、动态代理、脚本引擎、字节码框架都可能不断生成类。用类直方图/类加载日志定位,设置 MaxMetaspaceSize 只是兜底,不是根治。
04. 为什么虚拟线程不能用 jstack 看到?
jstack 展示的是 OS 线程(载体线程),虚拟线程由 JVM 调度、栈存储在堆上,不是 OS 线程。JDK 21 要用 jcmd Thread.dump_to_file -format=json 或 JFR 事件观察虚拟线程。
学习进度
记录保存在本机浏览器 · 不上传