「过早优化是万恶之源」,但生产环境的 Full GC 停顿 5 秒、OOM 导致服务重启,就不是过早优化了——是必须解决的问题。本文以 Java 21 为基础,梳理 JVM 调优的系统方法。
先测量,再调优
没有指标就调参等于盲人摸象。上线前必须采集的基线数据:
- 堆内存使用曲线(Eden / Survivor / Old Gen)
- GC 频率和停顿时间(P50 / P99)
- 线程数和 CPU 利用率
- 分配速率(allocation rate,MB/s)
推荐工具:async-profiler(CPU + 分配火焰图)、GCeasy(分析 GC 日志)、JFR(Java Flight Recorder,低开销持续采集)。
GC 选择:2026 年的答案
Java 21 提供四种主流 GC,选择策略:
- G1(默认):通用首选,停顿目标可控(
-XX:MaxGCPauseMillis=200),适合堆 4–32 GB - ZGC:停顿 < 1ms,适合大堆(> 32 GB)和低延迟服务。Java 21 已支持分代 ZGC
- Shenandoah:与 ZGC 类似,Red Hat 主导,OpenJDK 构建可用
- Parallel GC:吞吐优先的批处理场景,停顿不重要时用
# 低延迟 Web 服务推荐配置(Java 21 + 分代 ZGC)
java -XX:+UseZGC \
-XX:+ZGenerational \
-Xms4g -Xmx4g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/heapdump.hprof \
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags \
-jar app.jar
内存泄漏排查流程
Old Gen 持续增长且 Full GC 后不回落,是典型的内存泄漏信号:
- 触发
jcmd <pid> GC.heap_dump生成 heap dump - 用 Eclipse MAT 或 VisualVM 分析 Dominator Tree
- 找到占用最大的对象链,定位持有引用的根因
- 常见元凶:静态集合只增不减、ThreadLocal 未清理、缓存无过期策略
堆外内存与直接内存
Netty、RocksDB 等库大量使用 DirectByteBuffer,这部分内存不在堆内,OOM 时堆 dump 看不出问题:
- 监控
BufferPoolMXBean的直接内存使用量 - 设置
-XX:MaxDirectMemorySize限制上限 - 堆外泄漏排查用
Native Memory Tracking(-XX:NativeMemoryTracking=detail)
容器环境的 JVM 参数
在 Kubernetes 中运行 Java 应用需要注意:
- 使用 Container Support(Java 10+ 默认开启),JVM 自动识别 cgroup 内存限制
- 设置
-XX:MaxRAMPercentage=75.0而非固定-Xmx,适配不同环境的资源限制 - requests 和 limits 比值建议 1:1.5–2,避免 OOMKilled
- 优雅停机:
-XX:+ExitOnOutOfMemoryError+ preStop hook 等待请求排空
调优禁忌
不要复制网上的「万能参数」——别人的 GC 日志和你的工作负载完全不同。
- 不要在没有 STW 停顿问题时切换到 ZGC——G1 的吞吐通常更好
- 不要无脑增大堆——更大的堆意味着更长的 GC 停顿(G1 模式下)
- 不要忽略 Metaspace——动态类加载过多(如 Groovy 脚本、反射代理)会导致 Metaspace OOM
小结
JVM 调优的正确姿势:建立基线 → 定位瓶颈类型(CPU / 内存 / GC)→ 针对性调整 → 验证效果。90% 的场景下,选好 GC 算法 + 合理设置堆大小 + 修复内存泄漏,就足够了。