JVM 垃圾回收与性能调优实战
【摘要】 JVM 垃圾回收与性能调优实战本文深入剖析 JVM 内存结构与垃圾回收机制,帮助你理解 GC 原理并进行有效的性能调优。 JVM 内存结构┌─────────────────────────────────────────────────────────┐│ JVM ││ ┌─────...
JVM 垃圾回收与性能调优实战
本文深入剖析 JVM 内存结构与垃圾回收机制,帮助你理解 GC 原理并进行有效的性能调优。
JVM 内存结构
┌─────────────────────────────────────────────────────────┐
│ JVM │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 线程共享区域 │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 堆 Heap │ │ 方法区/元空间│ │ │
│ │ │ │ │ Metaspace │ │ │
│ │ │ 新生代+老年代│ │ 类元数据 │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 线程私有区域 │ │
│ │ 虚拟机栈 │ 本地方法栈 │ 程序计数器 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
堆内存分代(最核心):
┌─────────────────────────────────────────────────────────┐
│ 堆 Heap │
│ ┌───────────────────────────┐ ┌───────────────────┐ │
│ │ 新生代 Young │ │ 老年代 Old/Tenured│ │
│ │ ┌──────┐ ┌────┐ ┌────┐ │ │ │ │
│ │ │ Eden │ │ S0 │ │ S1 │ │ │ 长期存活对象 │ │
│ │ └──────┘ └────┘ └────┘ │ │ │ │
│ │ 8 1 1 (比例) │ │ │ │
│ └───────────────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘
对象的一生:从分配到回收
对象分配
Object obj = new Object();
// 1. 优先在 Eden 分配(TLAB:线程本地分配缓冲,减少锁竞争)
// 2. 大对象(超过 -XX:PretenureSizeThreshold)直接进老年代
// 3. 分配担保:Eden 不足时触发 Minor GC
可达性分析
GC Roots(根对象):
- 虚拟机栈中引用的对象
- 方法区静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈 JNI 引用的对象
从 GC Roots 出发做可达性分析:
可达 → 存活
不可达 → 可回收(还要经历"两次标记",给 finalize 机会)
对象晋升
新生代对象经历的 Minor GC 次数记录在对象头 Mark Word 中:
年龄 = 经历 Minor GC 且存活 = 1
... 累计到达阈值(-XX:MaxTenuringThreshold,默认 15)
特殊:动态年龄判定
Survivor 中同龄对象总和 > Survivor 空间一半
→ 大于等于该年龄的对象直接晋升老年代(避免 Survivor 溢出)
垃圾回收算法
┌─────────────────────────────────────────────────────────┐
│ 1. 标记-清除(Mark-Sweep) │
│ ✓ 简单 ✗ 产生内存碎片 │
│ │
│ 2. 标记-复制(Mark-Copy) │
│ ✓ 无碎片、效率高 ✗ 浪费一半空间(适合新生代) │
│ │
│ 3. 标记-整理(Mark-Compact) │
│ ✓ 无碎片 ✗ 移动对象开销大(适合老年代) │
│ │
│ 4. 分代收集:新生代用复制,老年代用整理/清除 │
└─────────────────────────────────────────────────────────┘
新生代回收(Minor GC / Young GC)流程:
Eden + S0 存活对象 ──复制──→ S1
清零 Eden 和 S0,S0 与 S1 角色互换
达到年龄阈值的对象晋升老年代
垃圾收集器演进
| 收集器 | 特点 | 分代 | 适用 |
|---|---|---|---|
| Serial | 单线程,Stop-The-World | 新生代 | 客户端/小内存 |
| ParNew | Serial 多线程版 | 新生代 | 配合 CMS |
| Parallel Scavenge | 关注吞吐量 | 新生代 | 后台计算 |
| Serial Old | 单线程,标记整理 | 老年代 | 客户端 |
| Parallel Old | 多线程,标记整理 | 老年代 | 吞吐优先 |
| CMS | 并发标记清除,低停顿 | 老年代 | 响应优先(已弃用) |
| G1 | 分 Region,可预测停顿 | 整堆 | 服务端默认 |
| ZGC | 染色指针,亚毫秒停顿 | 整堆 | 超大堆、低延迟 |
| Shenandoah | 并发压缩 | 整堆 | 低延迟 |
CMS 的四阶段
1. 初始标记(STW):标记 GC Roots 直接关联对象,快
2. 并发标记:与用户线程并行遍历,慢但不停顿
3. 重新标记(STW):修正并发期间的变动,快
4. 并发清除:与用户线程并行清理
问题:并发清除产生碎片(→ Full GC)、浮动垃圾、
Concurrent Mode Failure
G1 的分 Region 设计
堆被划分为等大小 Region(1~32MB):
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ S │ O │ O │ H │ E │ O │ E=Eden S=Survivor
└───┴───┴───┴───┴───┴───┴───┴───┘ O=Old H=Humongous(大对象)
特点:
1. 逻辑分代,物理不分代(Region 动态扮演角色)
2. 优先回收"垃圾最多"的 Region(Garbage First)
3. 可设置目标停顿时间 -XX:MaxGCPauseMillis
4. 用 Remembered Set 记录跨 Region 引用,避免全堆扫描
GC 日志与调优
关键参数
# 打印 GC 日志(JDK 9+ 统一日志)
-Xlog:gc*,gc+heap=info:file=gc.log:time,uptime,level,tags
# 堆大小
-Xms4g -Xmx4g # 生产建议初始=最大,避免动态扩缩
# 新生代
-Xmn1g # 新生代大小
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 元空间
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
# 晋升阈值
-XX:MaxTenuringThreshold=15
# 选择收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # G1 目标停顿
-XX:+UseZGC
# OOM 时导出堆快照
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump.hprof
读取 GC 日志
[GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76288K)]
65536K->8200K(251392K), 0.0123456 secs]
关键信息:
- 触发原因:Allocation Failure
- 新生代:65536K -> 8192K(回收后)
- 整堆:65536K -> 8200K
- 耗时:12ms
关注:
1. 频率:Minor GC 太频繁 → 新生代过小或对象分配过快
2. 耗时:单次停顿过长 → 影响响应时间
3. 晋升:过早晋升 → 老年代压力大,检查 Survivor 与阈值
4. Full GC:频繁 Full GC 是危险信号
实战:常见问题排查
问题 1:频繁 Full GC
现象:应用响应变慢,CPU 高,日志频繁 Full GC
排查:
1. 是否内存泄漏?老年代回收后仍高位不降
→ jmap -histo:live <pid> 看对象直方图
→ dump 堆用 MAT/VisualVM 分析 GC Roots 引用链
2. 是否大对象直接进老年代?
→ 检查是否有超大数组/集合
3. 是否元空间/Metaspace 溢出?
→ 频繁动态生成类(CGLIB、反射)
4. Survivor 过小导致过早晋升?
→ 调大 -Xmn 或 SurvivorRatio
问题 2:内存泄漏定位
# 1. 查看堆概况
jmap -heap <pid>
# 2. 对象直方图(live 会触发 Full GC)
jmap -histo:live <pid> | head -30
# 3. 导出堆快照
jmap -dump:live,format=b,file=heap.hprof <pid>
# 4. 用 MAT 打开,找 Dominator Tree 和大对象的 GC Roots
问题 3:CPU 飙高定位
# 1. 找到高 CPU 线程
top -Hp <pid>
# 2. 线程 ID 转十六进制
printf '%x' <tid>
# 3. 导出线程栈并查找
jstack <pid> | grep -A 30 <hex_tid>
常用工具
| 工具 | 用途 |
|---|---|
| jps | 查看 Java 进程 |
| jstat | 实时 GC 统计 jstat -gcutil <pid> 1000 |
| jmap | 堆快照、对象直方图 |
| jstack | 线程栈(死锁、CPU 定位) |
| jinfo | 查看/修改 JVM 参数 |
| MAT / VisualVM / Arthas | 堆分析与线上诊断 |
调优最佳实践
- 先测量,后调优:用数据说话,不要凭感觉改参数
- Xms = Xmx:避免堆动态伸缩带来的开销与抖动
- 优先选 G1:JDK 9+ 默认,通用性好;超大堆低延迟选 ZGC
- 设置合理停顿目标:如
-XX:MaxGCPauseMillis=200,但别设过小(会增加 GC 频率) - 开启 HeapDumpOnOutOfMemoryError:OOM 现场可回溯
- 监控 GC 指标:停顿时间、频率、吞吐、晋升速率,接入监控告警
- 容器场景对齐内存:
-Xmx要与容器 limits 匹配,JDK 8u191+ 支持-XX:+UseContainerSupport
总结
| 维度 | 要点 |
|---|---|
| 内存结构 | 堆分新生代(Eden+S0+S1)与老年代 |
| 回收判定 | 可达性分析,GC Roots 可达即存活 |
| 算法 | 新生代复制,老年代整理/清除 |
| 收集器 | G1 通用,ZGC 低延迟超大堆 |
| 调优核心 | 减少 Full GC、控制停顿、避免泄漏 |
JVM 调优的本质:在吞吐量、停顿时间、内存占用三者间找平衡,一切以压测和监控数据为依据。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)