一,JVM如何判断一个对象是“不可达”的
1,Stop The World(STW)
GC开始前,暂停所有用户线程,防止引用结构在扫描过程中发生变化,避免“你扫我改”。
2,Root Scanning(根集合扫描)
JVM会收集所有被直接引用的对象作为 GC Roots(根对象),形成一个 Root Set,包括:
- 所有线程栈帧中的局部变量(包括main线程和其他线程)
- Metaspace中的静态字段引用
- 运行时常量池中的对象引用
- JNI本地方法中的对象引用
- 类加载器自身持有的引用
3,可达性分析(Root Tracing)
从这些 GC Roots 出发,沿着引用链递归遍历堆上的对象,对能访问的对象打上“可达”的标记。
4,扫描整个堆内存:
- 没有被打上标记的对象即为不可达,就是垃圾对象,将成为GC的目标。
二,GC算法
1,Mark-Sweep(标记清除)
- 第一遍扫描标记所有可达对象(活对象)
- 第二遍扫描堆,清除没有被标记的
缺点:内存碎片化,影响后续对象的连续分配效率。
2,Copying(拷贝清除)
- 将新生代内存被分成了Eden区,From Survivor区,To Survivor区,比例默认为8:1:1。
- GC过程中,将活跃对象从Eden区,From Survivor区拷贝到To Survivor区。
- 复制完成后,From Survivor区和To Survivor区身份逻辑互换,为下一次GC做准备。
- 如果To Survivor区存活对象太多(大于容量一半),会触发动态年龄判断,提前将部分年龄较大的直接晋升为老年代。
优点:只扫描一次,效率高,且没有碎片。
缺点:移动复制对象,需要调整对象引用;始终保持一个Survivor区为空,浪费小部分空间。
适用场景:关于Copying算法,更适用于新生代对象存活率低的场景,在GC完存活对象大于新生代总容量的5%(To Survivor的一半,就是5%),就会触发提前晋升。
3,Mark-Compact(标记压缩)
标记压缩是将存活的对象内存的一端移动,让所有活的对象紧凑地排在一起,然后将剩余的空间全部清除。
- 第一次扫描,标记 + 计算目标位置
- 第二次扫描,移动对象
- 第三步,指针修正,更新所有引用指向新地址。
优点:内存紧凑,无碎片
缺点:效率慢,需要两次扫描+一次修正
适用场景:老年代
三,垃圾回收器组合
1,Serial + Serial Old
Java最早,最基础的垃圾回收器就是Serial + Serial Old组合。
- GC类型:
- 单线程 + STW
- 算法:
- Serial:新生代 - 复制算法
- Serial Old:老年代 - 标记清除算法
- 优点:
- 简单可靠,实现简单
- 缺点:
- 单线程,效率低,GC停顿时间长
- 适用场景:
- 轻量级应用,小堆内存
2,Parallel Scavenge + Parallel Old
- GC类型:
- 多线程 + STW
- 算法:
- PS:新生代 - 复制算法
- PO:老年代 - 标记整理算法。
- 优点:
- 并行执行,充分利用多核CPU资源,GC速度快。
- GC干净利落,次数相对少。
- 缺点:
- STW停顿时间长,不适合对响应时间要求高的场景
- 适用场景:
- 吞吐量优先的批处理或后台任务,对响应速度要求不高的场景。
3,Parallel New + CMS
- GC类型:
- PN 多线程 + STW,CMS 大部分并发
- 算法:
- PN:新生代 - 复制算法
- CMS:老年代 - 标记清除算法
- 优点:
- 响应速度快,大部分阶段和应用线程并发执行
- 缺点:
- 会产生浮动垃圾
- 内存碎片化严重
- 清理速度慢时会触发Full GC(严重卡顿)
- 适用场景:
- Web 等服务对响应时间要求高的系统
PS + PO 与 PN + CMS 的对比
| 特性 | PS + PO | PN + CMS |
|---|---|---|
| 新生代收集器 | Parallel Scavenge | ParNew |
| 老年代收集器 | Parallel Old | CMS(Concurrent Mark Sweep) |
| 并发能力 | 全 STW,多线程并行 GC | 大部分并发,仅部分 STW |
| 回收算法 | 新生代复制 + 老年代整理 | 新生代复制 + 老年代清除 |
| 吞吐量 | 高,适合批处理 | 中等 |
| 响应时间 | 一次 GC 停顿较长 | 停顿短,响应更好 |
| 缺点 | STW 时间长 | 容易产生碎片,浮动垃圾,可能触发 Full GC |
| 适用场景 | 后台任务,吞吐量优先 | Web 服务,交互响应优先 |
四,CMS
CMS的四个阶段
-
初始标记(Initial Mark)
- STW
- 只标记 GC Roots 直接可达的对象,停顿极短
-
并发标记(Concurrent Mark)
- 与应用线程并发执行
- 从初始标记的对象出发递归扫描整个对象图,标记所有可达对象
- 用的是三色标记法,黑(标记完成),灰(待处理),白(未标记)
- 问题:并发标记期间引用结构可能被修改,产生“误判”
- 解决:利用写屏障 + 卡表机制记录并发期间对象引用变化(即增量更新)
-
重新标记(Remark)
- STW
- 对记录的引用变化对象进行“补标记”,确保不遗漏可达对象
-
并发清除(Concurrent Sweep)
- 与应用线程并发执行
- 清除未被标记的白色对象
- 这一阶段仍可能产生新的垃圾,称为浮动垃圾
CMS的核心问题
- 写屏障复杂,所有引用的写操作都要额外记录进卡表,影响应用线程性能。
- 浮动垃圾,清理时仍有线程产生新垃圾,清理不彻底,只能等下GC。
- Full GC退化严重,Full GC使用的是Serial Old,单线程标记清除,STW时间特别长。
- 碎片问题,CMS是标记清除算法,不移动对象,会产生大量碎片。
- 维护成本高,写屏障,卡表,增量更新逻辑复杂,代码难维护。
CMS是第一个实现低延迟的老年代GC,解决了响应时间差的问题,但核心问题太多,最终被G1取代。
五,G1
G1 是第一个取消了物理分代的垃圾回收器,它的核心思想是:
将整个堆划分为多个大小相同的Region,优先回收垃圾最多的 Region 。
G1 支持 Young GC 和 Mixed GC,但不再固定 Eden,Survivor,Old 区的位置,而是通过 Region 标签动态管理。
概念
Collection Set (CSet)
- 记录需要被回收的 Region 。Young GC 回收 Eden + Survivor,Mixed GC 加上 Old。
- 生命周期:GC Trigger 之后立刻构建,GC 完成后释放。
- 粒度:多个 Region
Remembered Set (RSet)
- 记录了其他Region中的对象到本Region的引用 。
- 生命周期:程序运行期间一直维护。
- 粒度:每个 Region
Card Table
- 记录了JVM中哪些内存区域发生了 “写引用” 操作。
- 生命周期:程序运行期间一直维护。
- 粒度:全局的 BitMap 表
SATB Buffer
- 在并发阶段,写屏障会将丢失引用指向的对象加到SATB Buffer里,在重新标记时扫描SATB Buffer,将里面的对象标为活对象。
- 生命周期:并发标记期间启用。
- 粒度:每个线程持有
G1的四个阶段
初始标记
- STW
- 标记 GC Roots
- 开启 SATB 模式:
并发标记
- 与应用线程并发执行
- 从GC Roots 开始遍历,三色标记
- 可能的问题:并发期间对象结构会变(引用断了或换了),导致对象“误删”
- 解决方案:写屏障记录两件事情,(假如 a.ref = b 变成 a.ref = c)
- 被断开的旧引用对象加入SATB Buffer,(将 b 加入 SATB Buffer)。
- 修改的字段所在 Card 标记为 Dirty。(将 a 所在的 Card 标为 Dirty)。
重新标记
- 再次 STW
- 扫描SATB Buffer,将快照时是活的对象,补救回来。(比如刚才的 b 对象)
- 扫描Dirty Card,更新RemSet,防止遗漏引用
回收
- 将 CSet 中活着的对象 复制到其他 Region。
- 被搬空的 Region,标记为 Free Region,等待重新分配。
- 整个 Region 粒度的回收,不再逐个清理对象。
G1 不是传统意义上的 标记 + 清除,而是 标记 + 复制 + 按 Region 回收。
295

被折叠的 条评论
为什么被折叠?



