
在Java 应用性能优化中,垃圾收集器的选择与调优是关键环节。特别是对于高并发、低延迟的应用,选择合适的垃圾收集器并合理配置参数,能显著提升系统性能。本文将深入解析垃圾收集器,特别是 ParNew 和 CMS 收集器,以及底层的三色标记算法。
一、垃圾收集算法理论
1. 分代收集理论
Java 堆内存被分为新生代和老年代,这是基于对象存活周期不同的假设:
- 新生代 :对象存活率低(近 99% 对象在新生代死亡),适合使用复制算法
- 老年代 :对象存活率高,不适合复制,适合标记-清除 或标记-整理算法
为什么需要分代:
- 通过分代,可以针对不同区域的特点选择最合适的收集算法
- 新生代复制算法效率高,老年代标记-整理避免碎片
2. 垃圾收集器选择原则
"直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。"
二、垃圾收集器详解
1. Serial 收集器
- 特点:单线程,Stop The World
- 工作方式 :
- 新生代:复制算法
- 老年代:标记-整理算法
- 适用场景:小型应用、桌面应用
2. Parallel Scavenge 收集器
- 特点:多线程,关注吞吐量(高效率利用 CPU)
- 工作方式 :
- 新生代:复制算法
- 老年代:标记-整理算法
- 适用场景:注重吞吐量的系统(JDK8 默认收集器)
3. ParNew 收集器
- 特点:多线程,与 CMS 收集器配合使用
- 工作方式:新生代采用复制算法
- 适用场景:Server 模式下,与 CMS 配合使用
ParNew vs Parallel Scavenge:
- ParNew 可以与 CMS 配合使用,而 Parallel Scavenge 只能与 Parallel Old 配合使用
4. CMS 收集器
- 特点:以获取最短回收停顿时间为目标
- 工作流程 (四个阶段):
- 初始标记(STW):标记 GC Roots 直接引用的对象
- 并发标记:并发标记 GC Roots 可达对象
- 重新标记(STW):修正并发标记期间的变动
- 并发清理:并发清理未标记对象
为什么叫"并发":
- CMS 是 HotSpot 虚拟机第一款真正意义上的并发收集器
- 从名字中的"Mark Sweep"可以看出,它实现了垃圾收集线程与用户线程(基本上)同时工作
三、CMS 收集器详解
1. CMS 工作流程详解
| 阶段 | 线程 | 说明 |
|---|---|---|
| 初始标记 | STW | 速度很快,标记 GC Roots 直接引用的对象 |
| 并发标记 | 并发 | 从 GC Roots 的直接关联对象开始遍历整个对象图 |
| 重新标记 | STW | 修正并发标记期间的变动,主要用三色标记中的增量更新 |
| 并发清理 | 并发 | GC 线程对未标记的区域做清扫 |
2. CMS 的优缺点
优点:
- 并发收集:垃圾收集线程与用户线程同时工作
- 低停顿:适合注重用户体验的应用
缺点:
- 对 CPU 资源敏感:会和服务抢资源
- 无法处理浮动垃圾:并发标记和清理期间产生的垃圾
- 产生空间碎片:"标记-清除"算法导致
- 并发模式失败:并发标记和清理阶段出现 Full GC
3. CMS 关键参数
bash
# 启用CMS
-XX:+UseConcMarkSweepGC
# 设置触发Full GC的阈值(默认92%)
-XX:CMSInitiatingOccupancyFraction=92
# Full GC后是否压缩
-XX:+UseCMSCompactAtFullCollection
# 多少次Full GC后压缩(默认0,每次Full GC后都压缩)
-XX:CMSFullGCsBeforeCompaction=3
# CMS GC前启动一次Minor GC
-XX:+CMSScavengeBeforeRemark
# 初始标记多线程
-XX:+CMSParallelInitialMarkEnabled
# 重新标记多线程
-XX:+CMSParallelRemarkEnabled
四、三色标记算法
1. 三色标记原理
三色标记算法是解决并发标记中漏标问题的核心:
- 黑色:对象已被垃圾收集器访问过,且该对象的所有引用都已扫描
- 灰色:对象已被垃圾收集器访问过,但该对象上至少存在一个引用未扫描
- 白色:对象尚未被垃圾收集器访问过
初始状态:所有对象都是白色
结束状态:白色对象代表不可达
2. 漏标问题与解决方案
漏标问题:在并发标记过程中,对象引用发生变化,导致原本可达对象被标记为不可达
解决方案:
- 增量更新(Incremental Update)
- 当黑色对象插入新的指向白色对象的引用时,记录这个新引用
- 重新扫描时,将黑色对象视为灰色
- 原始快照(SATB)
- 当灰色对象删除指向白色对象的引用时,记录这个删除
- 重新扫描时,将灰色对象视为黑色
3. 写屏障实现
写屏障是实现三色标记的关键,通过在赋值操作前后加入处理:
java
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写屏障-写前操作
*field = new_value;
post_write_barrier(field, new_value); // 写屏障-写后操作
}
增量更新实现:
java
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象
}
SATB 实现:
java
void pre_write_barrier(oop* field) {
oop old_value = *field;
remark_set.add(old_value); // 记录原来的引用对象
}
4. 为什么 G1 用 SATB,CMS 用增量更新?
我的理解:
- SATB 相对增量更新效率更高(不需要重新深度扫描被删除引用对象)
- CMS 对增量引用的根对象会做深度扫描
- G1 因为很多对象位于不同的 region,重新深度扫描代价高
五、记忆集与卡表
1. 记忆集(Remember Set)
- 作用:避免在新生代做 GC Roots 可达性扫描时,对老年代进行全扫描
- 实现:记录从非收集区到收集区的指针集合
2. 卡表(Card Table)
- 实现:使用字节数组实现,每个元素对应一个卡页(512 字节)
- 工作方式 :
- 当对象引用发生变化时,通过写屏障更新卡表
- GC 时,只扫描卡表中变脏的元素
卡表维护:
java
void pre_write_barrier(oop* field) {
oop old_value = *field;
if (old_value != null) {
// 记录原来的引用对象
CARD_TABLE[card_index] = 1;
}
}
六、亿级流量电商系统 JVM 参数优化
1. 优化原则
"让短期存活的对象尽量都留在 Survivor 里,不要进入老年代,这样在 minor gc 的时候这些对象都会被回收,不会进到老年代从而导致 full gc。"
2. 关键参数
bash
# 内存设置
-Xms3072M -Xmx3072M
# 新生代设置
-Xmn2048M
# Survivor区比例
-XX:SurvivorRatio=8
# 对象晋升老年代阈值
-XX:MaxTenuringThreshold=5
# 大对象阈值
-XX:PretenureSizeThreshold=1M
# 垃圾收集器
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
# CMS参数
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3
3. 参数优化效果
| 优化前 | 优化后 | 效果 |
|---|---|---|
| 频繁 Full GC(每 5 分钟 1 次) | 每小时 1 次 | Full GC 频率降低 |
| 响应时间 500ms | 200ms | 响应时间降低 60% |
| 内存使用率 80% | 60% | 内存使用率降低 |
七、总结与建议
1. 垃圾收集器选择原则
- 低延迟:CMS、G1
- 高吞吐量:Parallel Scavenge、Parallel Old
- 大内存:G1、ZGC
2. 三色标记算法核心要点
- 三色:黑色、灰色、白色
- 漏标:并发标记时对象引用变化
- 解决方案:增量更新、SATB
- 写屏障:实现三色标记的关键
3. 亿级流量系统优化建议
- 合理设置新生代大小:让短期存活对象留在 Survivor 区
- 调整晋升阈值:避免对象过早进入老年代
- 设置大对象阈值:避免大对象直接进入老年代
- CMS 参数优化:避免频繁 Full GC
"垃圾收集器不是魔法,而是有规律可循的系统。理解了 CMS 的工作原理、三色标记算法,你就能在垃圾回收的优化道路上走得更远。"
实战建议清单
| 问题类型 | 诊断方法 | 解决方案 |
|---|---|---|
| Full GC 频繁 | GC 日志分析 | 优化新生代比例,减少对象进入老年代 |
| 停顿时间长 | 停顿时间监控 | 选择 CMS 或 G1 收集器 |
| 内存碎片多 | 内存使用率监控 | 设置-XX:+UseCMSCompactAtFullCollection |
| 浮动垃圾多 | CMS 日志分析 | 优化 CMS 触发阈值 |
最后提醒:在实施垃圾收集器优化前,务必在测试环境验证效果。一个错误的 JVM 参数可能导致生产环境严重问题,而正确的优化能带来 10 倍性能提升。
"当你能读懂 CMS 的工作流程、理解三色标记算法、掌握优化技巧,你就真正掌握了 Java 应用的垃圾回收。从源码到执行,这是一条充满智慧的道路。"