内容纲要
标签:Java, JVM, GC, 性能优化, G1, ZGC, Shenandoah, JDK8, JDK11, JDK17, JDK21, Kubernetes, 容器化, JFR, async-profiler
目标:一份从入门到精通的 JVM 性能与稳定性实战手册,覆盖主流 LTS 版本(8/11/17/21),给出“可复制”的调优方法、参数模板、问题排查 SOP,与典型场景配方。看完即可上手,遇事有章可循。
0. 学会先“调思维”,再“调参数”
金律
- 先度量→再假设→小步调整→复测回归;
- 明确目标:吞吐(QPS/批处理速度)、延迟(p95/p99)、资源占用(内存/CPU/成本)、启动/冷启动;
- 记住:绝大多数“JVM问题”是“系统问题”(I/O、线程池、数据库、网络、容器限额),JVM 只是放大镜。
常见误区
- 只改 GC 参数不看 GC 日志/火焰图;
- 只看平均延迟不看 p99;
- 容器里忘了 cgroups 导致堆外挤爆;
- 误开对象池/字符串池,反而制造 GC 压力。
1. 一张图看懂 JVM(8/11/17/21)
运行时构成
- Java 堆:年轻代(Eden + Survivor)/ 老年代(或分区化,如 G1 分区)。
- MetaSpace(8+,替代 PermGen)
- Code Cache(JIT 编译后的机器码)
- 线程栈(-Xss)
- 堆外内存(直接缓冲区、JNI、NIO、ZGC/Shenandoah 元数据等)
GC 矩阵
- JDK 8:Parallel(默认)、CMS(老牌低停顿,后续移除)、G1(可选,8u20+ 支持 String Dedup)。
- JDK 11:G1(默认),ZGC(实验→15 起转正),Shenandoah(OpenJDK 较早可用),Epsilon(无回收)。
- JDK 17:G1(默认),ZGC(稳定)、Shenandoah(稳定)。
- JDK 21:G1(默认),ZGC 代际化(Generational ZGC),Shenandoah(含代际变体),虚拟线程(Loom)进入 GA,JIT 与 CDS 更完善。
日志与容器
- JDK 8:旧 GC 日志(
-XX:+PrintGCDetails -Xloggc
)。容器感知需 8u191+ 搭配-XX:+UseCGroupMemoryLimitForHeap
(早期)或使用百分比/分数参数。 - JDK 9+:统一日志(
-Xlog:gc*
),-XX:MaxRAMPercentage
等百分比参数,cgroups v2 支持完善(14+)。
2. 选择 GC 的“决策树”(实战口径)
- 延迟优先(p99 < 50ms 或更苛刻)
- JDK 17/21:首选 ZGC(大堆/超低停顿)或 Shenandoah(低停顿、易调)。
- JDK 8/11:G1(设
MaxGCPauseMillis=50~200ms
),避免 CMS 新建项目。
- 吞吐优先(批处理/离线计算)
- Parallel GC(8/11/17/21 均可),多核充分,停顿可接受。
- 通用在线服务(微服务)
- JDK 11+:G1 默认足够好;堆较大、延迟敏感可上 ZGC(17/21)。
- 超大堆(> 32 GB)/ 极端低抖动
- ZGC(17/21) 优先。
3. 可靠的内存配额与容量规划(含容器/K8s)
3.1 统一口径(经验安全线)
Pod/容器 Memory 限额 = L
预留(MetaSpace + 代码缓存 + 线程栈 + 直接内存 + GC/其他)= R(建议 25% × L,至少 512 MiB)
建议-Xmx = L - R
,并-Xms = -Xmx
保持稳定
细项估算
- 线程栈:
线程数 × Xss
(默认 ~1 MB/线程,虚拟线程不占用传统 1:1 栈,但仍有调度开销) - 直接内存(Netty 等):显式设置
-XX:MaxDirectMemorySize
,否则可能过度分配 - MetaSpace:典型 100–512 MiB(取决于类数量/动态代理)
- 代码缓存:64–256 MiB(视 JIT 活跃度)
容器标配(JDK 11+)
-XX:InitialRAMPercentage=50 \
-XX:MaxRAMPercentage=70 \ # 70% 给堆
-XX:MinRAMPercentage=50 \
-XX:MaxDirectMemorySize=512m \ # Netty/DirectBuffer 显式上限
-XX:+AlwaysPreTouch # 大堆避免运行时缺页抖动(仅内存足够时)
JDK 8(老项目)
- 8u191+ 推荐:
-XX:MaxRAMFraction=1 -XX:InitialRAMFraction=2
或显式-Xmx/-Xms
; - 早期容器需:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
(仅在旧 8u 上)。
4. 版本关键差异与“坑位图”
- Biased Locking:15 起默认禁用,18 起删除 → 不再依赖相关开关。
- Compact Strings(9+):自动按 Latin-1/UTF-16 存储,提高字符串密度。
- String Dedup(G1 专属):
-XX:+UseStringDeduplication
(8u20+,9+ 继续有效),换 CPU 降 GC 压力。 - CDS/AppCDS:11+ 开源易用;21 上生成与加载更顺滑,显著提升启动/冷启动。
- 统一日志(9+):一套
-Xlog:
覆盖 GC/安全点/类加载等。 - ZGC/Shenandoah:11 起可用,15/17 转正更稳;21 引入代际 ZGC,改善短命对象吞吐与内存回收效率。
- 虚拟线程(21):海量并发编程模型改变线程调优方式(详见 §10)。
5. GC 目标与关键开关(按 GC 策略)
5.1 G1(推荐通用、低风险)
常用:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100 # 50~200 之间按 SLA 设
-XX:InitiatingHeapOccupancyPercent=30 # 触发并发标记的阈值
-XX:G1ReservePercent=20
-XX:ParallelGCThreads=<CPU> # 一般自动;容器 CPU 低时可显式
-XX:ConcGCThreads=<CPU/4..CPU/2>
-XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeRSetStats -XX:+G1SummarizeConcMark
# 字符串去重
-XX:+UseStringDeduplication
5.2 ZGC(低停顿/大堆/21 代际)
-XX:+UseZGC
# 代际(21)
# 默认已代际化,如需软上限:
-XX:SoftMaxHeapSize=<N> # 动态控制堆扩张倾向
# 可配:
-XX:ZCollectionInterval=... # 最大间隔触发
-XX:ZUncommitDelay=... # 空闲内存回退
一般不需要也不建议对 ZGC 做复杂“手调”,观测 + 负载对齐更重要。
5.3 Shenandoah(低停顿、易用)
-XX:+UseShenandoahGC
# 代际变体(视发行版/版本)
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
5.4 Parallel(吞吐优先/批处理)
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=200~500
-XX:ParallelGCThreads=<CPU>
6. GC 日志与可观测性(8 vs 9+)
6.1 JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause \
-Xloggc:/logs/gc.log \
-XX:+PrintGCApplicationStoppedTime \
-XX:+PrintTenuringDistribution \
-XX:+PrintAdaptiveSizePolicy \
-XX:+PrintGCID
6.2 JDK 9+
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
# 常用子系统
-Xlog:safepoint=info:stringtable=info:class+load=info,class+unload=info
怎么看?
- 关注 STW 时间(GC/安全点),晋升失败/并发失败,混合回收时长,回收比率,浮动垃圾;
- 观察 Young/Old 分配速率 与 晋升速率 是否匹配对象存活特征;
- 以 趋势 为主,结合业务流量曲线。
7. 标准工具箱(命令速查)
线上“十分钟”取证清单
# 1) 进程/资源
top -H -p <pid> # 线程占用
pmap -x <pid> | tail # 虚拟内存分布
cat /sys/fs/cgroup/... # 容器限额核查
# 2) 线程/栈
jcmd <pid> Thread.print
jstack -l <pid> > threads.txt
# 3) 堆/类
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram > histo.txt
jmap -histo <pid> | head
jcmd <pid> VM.native_memory summary # NMT 需 -XX:NativeMemoryTracking=summary/detail
# 4) 即刻开 JFR(11+)
jcmd <pid> JFR.start name=onfly settings=profile duration=120s filename=/tmp/onfly.jfr
# 或停止/导出
jcmd <pid> JFR.dump name=onfly filename=/tmp/onfly.jfr
jcmd <pid> JFR.stop name=onfly
# 5) 导出堆(大文件注意磁盘)
jmap -dump:live,file=/tmp/heap.hprof <pid>
火焰图(Linux,生产通用)
async-profiler
(CPU/Wall/分配/锁),命令示例:
./profiler.sh -e cpu -d 60 -f out.svg <pid>
8. 典型场景“黄金模板”(可直接套用)
8.1 延迟敏感 API(p99 ≤ 100ms),JDK 17/21 + G1
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=30
-XX:G1ReservePercent=20
-XX:+UseStringDeduplication
-XX:+ParallelRefProcEnabled
-Xms=4g -Xmx=4g # 与容器配额联动
-XX:MaxRAMPercentage=70
-XX:MaxDirectMemorySize=512m
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps
8.2 极低停顿(金融撮合/交易风控),JDK 21 + ZGC
-XX:+UseZGC
-XX:SoftMaxHeapSize=8g
-Xms=8g -Xmx=8g
-XX:MaxRAMPercentage=70
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError
8.3 批处理/离线任务(吞吐优先),JDK 11/17 + Parallel
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=400
-Xms=16g -Xmx=16g
-XX:ParallelGCThreads=<CPU>
-Xlog:gc*:file=/logs/gc.log:time,uptime
8.4 Netty/网关(直接内存高),JDK 17 + G1
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=30
-Xms=4g -Xmx=4g
-XX:MaxDirectMemorySize=1g # 显式限制 DirectBuffer
-XX:+DisableExplicitGC # 避免误调 System.gc()
-Dio.netty.leakDetection.level=paranoid # 漏洞排查期
8.5 容器/微服务(通用基线),JDK 11/17
-XX:+UseG1GC
-XX:InitialRAMPercentage=50
-XX:MaxRAMPercentage=70
-XX:MinRAMPercentage=50
-XX:MaxDirectMemorySize=256m
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
9. 稳定性“保险丝”
-XX:+ExitOnOutOfMemoryError
:OOM 直接退出,避免“假活着”;-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps
:留证;-XX:NativeMemoryTracking=summary
:堆外排查(需开销,可按需打开);- 限速与熔断:线程池/队列/连接池上限明确;
- 显式 MaxDirectMemorySize:避免 Netty 类库默认“想用多少用多少”。
10. 代码与 JIT 调优要点
- 逃逸分析 & 标量替换:让对象“栈上分配”。减少同步与分配压力(避免新建临时大对象)。
- TLAB:线程本地分配缓冲,默认即可;极端场景再看
-XX:-ResizeTLAB
。 - 内联/去优化:过深层级影响 JIT,关注火焰图的
vtable
调用与 megamorphic call。 - 并发工具:热点整数累加用
LongAdder
;注意@Contended
(避免伪共享,8 需-XX:-RestrictContended
)。 - 字符串:9+ 已 Compact Strings,一般无需手工“池化”;G1 可开 String Dedup。
- 对象池:现代 JVM 下多数对象池是“反模式”(连接池除外)。
- 虚拟线程(JDK 21):
- 大幅提升阻塞型 I/O 并发能力;
- 避免同步阻塞把载体线程(carrier)“钉死”(如
synchronized
长持有/本地调用阻塞),尽量采用非阻塞 I/O; - 线程池调参转为限制请求层面并发(信号量/背压),减少手动池大小微调。
11. JFR/火焰图排查“SOP”
当你遇到 p99 抖动 / CPU 飙升 / GC 频繁 / OOM 先兆:
- 采样
jcmd JFR.start settings=profile duration=120s filename=/tmp/now.jfr
- 同时跑
async-profiler
:./profiler.sh -e cpu -d 60 -f cpu.svg <pid>
- 看图
- 热点在用户代码?库?系统调用?锁争用还是分配风暴?
- 分配火焰图若在某模型/DTO 构造 → 减少短命对象/复用缓冲。
- 对症
- 分配过高 → G1/ZGC 均会更勤快;优先从代码/缓存降压;
- 锁热点 → 优化粒度/使用无锁容器/减少共享写;
- I/O 等待 → 线程爆炸?切到虚拟线程或改善下游吞吐。
12. 常见疑难杂症与对策
症状 | 观察 | 快速定位 | 处理建议 |
---|---|---|---|
Heap OOM | GC 日志晋升失败/Full GC 后仍满 | jmap -histo , JFR 对象存活 |
修复泄漏、降缓存上限、增堆、限并发 |
MetaSpace OOM | Metaspace 增长不回落 |
jcmd VM.native_memory detail , classloader 统计 |
减少动态代理/热加载,限制插件,升级框架 |
频繁 Young GC | 分配速率过高 | GC 日志分配速率,JFR Allocation | 降对象 churn,调小 MaxGCPauseMillis 并观察 |
老年代膨胀 | 存活对象多、晋升快 | PrintTenuringDistribution /JFR |
提高 Survivor,优化生命周期,调 IHOP(G1) |
STW 突刺 | safepoint 过长 | -Xlog:safepoint |
避免大规模类卸载、缩小 CodeCache 抖动、定位慢操作 |
Native OOM / 被 OOMKiller | cgroup 限额+堆外膨胀 | NMT ,/sys/fs/cgroup |
显式 MaxDirectMemorySize ,减线程栈,留足 R |
直接内存泄漏 | Netty 报 warn/GC 正常 | Netty LeakDetector | 升级/修复引用/确保 release() |
13. 启动/冷启动优化(CDS / AppCDS)
快速做法(JDK 11/17/21)
- 生成 CDS:
java -Xshare:dump
(基础) - AppCDS:使用 JFR 录制类列表 →
jcmd VM.classloaders
/工具导出,生成自定义归档; - 运行:
-Xshare:on
,显著缩短启动与暖机时间(微服务冷启收益明显)。
14. 线程池与并发治理(线上可靠性)
- 所有线程池 有界 + 拒绝策略 明确(记录/回退/降级);
- 拆分 CPU 密集/IO 密集池,避免互相饥饿;
- 虚拟线程时代:不再用巨大固定池,转为按请求限流 + 背压。
15. 参数“口袋书”(JDK 分版本速查)
15.1 JDK 8(常见)
# GC
-XX:+UseG1GC | -XX:+UseParallelGC | -XX:+UseConcMarkSweepGC(老项目)
-XX:+UseStringDeduplication # 仅 G1
# 堆与容器
-Xms/-Xmx 或 -XX:MaxRAMFraction / InitialRAMFraction
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap (非常旧的 8u)
# 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/logs/gc.log
# 诊断
-XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError
15.2 JDK 11/17/21(统一)
# GC
-XX:+UseG1GC (默认) | -XX:+UseZGC | -XX:+UseShenandoahGC | -XX:+UseParallelGC
# 容器百分比
-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70 -XX:MinRAMPercentage=50
-XX:MaxDirectMemorySize=512m
# 统一日志
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags
-Xlog:safepoint=info
# 保险
-XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError
16. 三类“端到端”演练
A. 高并发接口 p99 抖动
- 收集:
JFR.start 120s
+-Xlog:gc*
; - 分析:火焰图若显示 JSON 序列化分配风暴 → 启用复用缓冲/减少中间对象;
- 调参:G1 降
MaxGCPauseMillis
→100,IHOP
从 45→30; - 复测:看 p99 与 GC STW 总时长,确保不反向波动。
B. Netty OOMKill(容器)
- 发现:Pod 被 OOMKill,GC 正常;
- 定位:
NMT summary
+ Netty 泄漏检测; - 处理:
-XX:MaxDirectMemorySize=1g
,调低Xmx
,升级 Netty/修正 release; - 预防:容器预留 R≥25%,Prometheus 监控直接内存与映射失败。
C. 大堆批处理速度慢
- 观测:CPU 低,GC 不频繁;
- 原因:单线程/锁粒度粗/IO 限速;
- 方案:切 Parallel GC,放大
ParallelGCThreads
,并行化任务,批量 I/O; - 验证:单位数据处理时间显著下降、Full GC 仍可接受。
17. 落地流程(团队级)
- 建立基线:统一 GC 模板、日志格式、JFR 取证模板;
- SLA 看板:p50/p95/p99、GC STW、分配速率、年轻/老年代占比;
- 容量演练:压测得到“资源-吞吐”曲线,标记经济高点;
- 问题库:沉淀“症状→证据→处方→工单链接”,上线前过 checklist。
18. 学习路径(从 0 到 1 到 10)
- 0→1:掌握 GC 分类、常用参数、会读 GC 日志 + JFR 火焰图;
- 1→5:能给不同场景选 GC 并解释取舍;容器化容量规划不踩坑;
- 5→10:结合业务特征做代码级降压、跨层诊断(DB/缓存/网络),把调优固化为模板与规范。
附:你可能会用到的“命令备忘”
# 统一日志(9+)
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags
-Xlog:safepoint=info
# JFR(11+)
jcmd <pid> JFR.start name=profile settings=profile delay=10s duration=120s filename=/tmp/profile.jfr
# 堆/类/线程
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> Thread.print
# 堆 dump
jmap -dump:live,file=/tmp/heap.hprof <pid>
# NMT
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary
结语
JVM 调优不是堆参数清单,而是“业务目标→系统画像→证据驱动→小步验证”的工程化能力。把本手册的模板与 SOP 嵌入你的 CI/CD 与运维流程,你的 Java 服务会更“稳、快、省”。