JVM垃圾收集器与三色标记算法详解

在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 收集器

  • 特点:以获取最短回收停顿时间为目标
  • 工作流程 (四个阶段):
    1. 初始标记(STW):标记 GC Roots 直接引用的对象
    2. 并发标记:并发标记 GC Roots 可达对象
    3. 重新标记(STW):修正并发标记期间的变动
    4. 并发清理:并发清理未标记对象

为什么叫"并发":

  • 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. 漏标问题与解决方案

漏标问题​:在并发标记过程中,对象引用发生变化,导致原本可达对象被标记为不可达

解决方案​:

  1. 增量更新(Incremental Update)
    • 当黑色对象插入新的指向白色对象的引用时,记录这个新引用
    • 重新扫描时,将黑色对象视为灰色
  2. 原始快照(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. 亿级流量系统优化建议

  1. 合理设置新生代大小:让短期存活对象留在 Survivor 区
  2. 调整晋升阈值:避免对象过早进入老年代
  3. 设置大对象阈值:避免大对象直接进入老年代
  4. CMS 参数优化:避免频繁 Full GC

"垃圾收集器不是魔法,而是有规律可循的系统。理解了 CMS 的工作原理、三色标记算法,你就能在垃圾回收的优化道路上走得更远。"

实战建议清单

问题类型 诊断方法 解决方案
Full GC 频繁 GC 日志分析 优化新生代比例,减少对象进入老年代
停顿时间长 停顿时间监控 选择 CMS 或 G1 收集器
内存碎片多 内存使用率监控 设置-XX:+UseCMSCompactAtFullCollection
浮动垃圾多 CMS 日志分析 优化 CMS 触发阈值

最后提醒​:在实施垃圾收集器优化前,务必在测试环境验证效果。一个错误的 JVM 参数可能导致生产环境严重问题,而正确的优化能带来 10 倍性能提升。

"当你能读懂 CMS 的工作流程、理解三色标记算法、掌握优化技巧,你就真正掌握了 Java 应用的垃圾回收。从源码到执行,这是一条充满智慧的道路。"

相关推荐
沐欣工作室_lvyiyi2 小时前
IIR数字带通滤波器(论文+源码)
算法·matlab·毕业设计·数字滤波器
zh_xuan2 小时前
LeeCode 61. 旋转链表
数据结构·c++·算法·leetcode·链表
tobias.b2 小时前
408真题解析-2010-8-数据结构-拓扑排序
数据结构·算法·计算机考研·408真题解析
Anastasiozzzz2 小时前
Redis脑裂问题--面试坑点【Redis的大脑裂开?】
java·数据库·redis·缓存·面试·职场和发展
tkevinjd2 小时前
4-初识Maven
java·maven
ckhcxy2 小时前
抽象类和接口(二)
java
短剑重铸之日2 小时前
《SpringCloud实用版》 Gateway 4.3.x 保姆级实战:路由 + 限流 + 鉴权 + 日志全覆盖
java·后端·spring cloud·架构·gateway
源代码•宸2 小时前
Golang原理剖析(彻底理解Go语言栈内存/堆内存、Go内存管理)
经验分享·后端·算法·面试·golang·span·mheap
高山上有一只小老虎2 小时前
mybatisplus实现简单的增删改查方法
java·spring boot·后端