垃圾标记
是垃圾回收的第一阶段,也是垃圾回收算法中关键的一步。它的主要目的是通过遍历内存中存活的对象,来标记哪些对象仍是可达的(即活跃的),哪些对象是不可达的(既可以被垃圾回收)。
标记的结果,将作为下一步(清理,压缩)的依据,确定哪些对象需要被释放或者移动
垃圾回收策略
- 引用计数法
原理:
- 每个对象维护一个 引用计数器,用于记录该对象被引用的次数。
- 当对象被创建或被引用时,计数器增加。
- 当引用被移除或销毁时,计数器减少。
- 如果计数器为零,说明该对象不可达,可以被回收。
实现流程:
- 初始化时,计数器设为 0。
- 每当有新的引用指向该对象时,计数器 +1。
- 每当引用消失时,计数器 -1。
- 如果计数器为 0,该对象会被立即回收。
缺点:
循环引用问题:
- 如果两个或多个对象相互引用,但没有其他外部对象引用它们,则它们的计数器始终大于零,导致无法被回收。
- 例如:
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
维护开销大:
- 每次引用变化都需要更新计数器,可能会影响性能。
- 根搜索法
原理:
- 从一组称为 GC Roots 的根对象开始,通过引用关系遍历对象图。
- 如果某个对象可以通过 GC Roots 找到,则认为它是可达的;否则认为它是不可达的,可以被回收。
实现流程:
- 从 GC Roots 出发,标记所有可以访问到的对象。
- 遍历整个堆,将未被标记的对象视为垃圾,进行回收。
优点:
无循环引用问题:
- 可达性分析仅关注对象是否能从根集合到达,不会因循环引用无法回收。
适用于复杂的引用关系:
- 能处理对象图中存在大量交叉引用的复杂场景。
分代垃圾回收的基础:
- 新生代、老年代的分代策略都基于此方法。
缺点:
暂停应用线程:
- 传统的垃圾收集器需要暂停所有应用线程(Stop-the-World),执行可达性分析,可能导致停顿时间较长。
实现复杂:
- 需要额外的辅助数据结构和更复杂的遍历逻辑。
分代回收策略
- 新生代 GC(Minor GC)
触发条件:
- 新生代的 Eden 区满时触发。
回收策略:
- 采用 复制算法:
- 将 Eden 区和一个 Survivor 区中的存活对象复制到另一个 Survivor 区或老年代。
- 减少内存碎片问题,适合对象存活时间短的特点。
特点:
- 回收速度快,代价低。
- 老年代 GC(Major GC / Full GC)
触发条件:
- 老年代内存不足时触发。
回收策略:
通常采用 标记-清除算法 或 标记-整理算法:
- 标记-清除算法:标记存活对象,清除无用对象,但可能会产生碎片。
- 标记-整理算法:在标记存活对象后,将存活对象移动到一侧,整理内存,避免碎片。
特点:
- 回收速度较慢,代价高,可能会导致应用长时间停顿。
垃圾回收器
- Serial GC
特点:
- 单线程工作
- 适用于单核环境和小堆内存(100MB)
- 新生代使用复制算法,老年代使用标记-整理算法
优点:
- 实现简单,适合小型应用
缺点
- 垃圾回收期间会停止所有应用线程(STW),延迟较高
适用环境
- 单线程,内存较小的应用
- Parallel GC
特点:
- 多线程执行垃圾回收,提升回收速度
- 注意吞吐量(应用运行时间与垃圾回收时间的比值)
- 新生代使用复制算法,老年代使用标记-整理算法
优点:
- 吞吐量高,适合多核CPU
缺点:
- STW时间比较长,不适合低延迟的场景
使用场景:
- 后台计算型任务,大批量数据处理应用
- CMS GC(Concurrent Mark-Sweep)
特点
- 专注于低延迟,适合对响应时间要求高的应用
- 老年代使用标记-清理算法,新生代使用复制算法
老年代回收分为以下阶段
- 初始阶段:标记GC Roots可达的对象(短暂暂停)
- 并发标记:多线程遍历对象图
- 重新标记:修正并发标记阶段的变动对象(短暂暂停
- 并发清楚:清楚不可达对象
优点
- 停顿时间短,适合交互型应用。
缺点
- 容易产生内存碎片
- 并发清理阶段可能导致系统吞吐量下降
使用场景
- 低延迟、高响应的应用,如Web服务器
- G1 GC(Garbage First)
特点:
- 通过将堆划分为多个区域,以区域为单位回收
- 同时支持新生代和老年代的回收(混合回收)
- 停顿可控:允许用户设置最大停顿时间目标
回收过程
- 初始标记:标记 GC Roots 直接可达的对象。
- 并发标记:遍历堆中的对象图,标记存活对象。
- 筛选回收:按回收收益优先级回收部分区域。
- 清理:整理内存,释放区域。
优点:
- 减少老年代碎片。
- 可控的停顿时间。
缺点:
- 相比 CMS,初期调优难度较高。
使用场景:
- 大堆内存、多核 CPU 的低延迟场景。
- ZGC(Z Garbage Collector)
特点:
- 超低延迟垃圾回收器,目标是将停顿时间控制在10ms以下
- 使用染色指针标记对象状态
回收过程
- 并发标记:找到存活对象
- 并发转移:将存活对象复制到新区域
- 并发清除:清除垃圾对象
- 支持非常大的堆内存(TB 级别)
优点:
- 停顿时间极低,适合实时性要求高的场景
缺点:
- 初期内存占用比较高
- 仅支持64位系统
使用场景:
- 超低延迟需求的应用,如金融交易系统
- Shenandoah GC
特点:
- 和ZGC类似,目标是低延迟,减少STW时间
- 通过并发整理老年代对象,避免碎片化
优点:
- 和 G1 相比,停顿时间更短
缺点:
- 内存开销高
使用场景:
延迟敏感的服务端应用
垃圾标记的过程
- 从根集合(GC Roots)出发:
- 垃圾标记阶段以一组成为GC Roots的引用集合为起点。
常见的GC Roots包括:
- Java栈中的局部变量
- 方法区的静态变量和常量
- 本地方法栈中的JNI引用
通过引用关系遍历对象图:
- 以GC Roots为起点,通过深度优先或者广度优先的方式遍历所有可达的对象。
- 遍历到的对象会被标记为“存活”状态
标记不可达对象:
- 未被访问到的对象会被认为是垃圾,标记为“不可达”
垃圾标记的方式
垃圾标记的具体实现方式可能因垃圾回收算法不同而异。常见的标记方式有:
标记-清除算法(Mark-Sweep)
- 标记阶段:从GC Roots出发,遍历并标记所有存活的对象
- 清除阶段:扫描堆内存,清理未被标记的对象
缺点:
- 标记和清理后会产生很多的碎片
标记-复制算法(Mark-Copy)
- 将可达对象复制到新的区域,未被复制的对象被认为是垃圾
优点:
- 避免内存碎片,但需要额外的内存空间
标记-整理算法(Mark-Compact)
- 标记阶段:标记所有存活的对象
- 整理阶段:将存活的对象压到堆的一端,清理无用对象,释放空间
优点:
- 解决了内存碎片的问题
三色标记法(多线程GC的经典方式)
- 白色:尚未访问的对象(默认状态,潜在垃圾)
- 灰色:已访问但未处理完引用的对象
- 黑色:已访问且其引用已处理完的对象
- 标记完成后,白色对象即为垃圾,可以被回收
GC Roots的作用
- GC Roots是垃圾标记的起点,决定了哪些对象是根可达的
可通过以下途径访问到的对象会被视为存活:
- 栈中的局部变量
- 静态属性引用的对象
- 常量引用的对象
- JNI(本地方法接口)中引用的对象
标记技术的优化
现代 GC 使用了多种技术优化垃圾标记的性能:
增量标记
- 讲标记过程拆分成多个小步骤,减少GC的事件
并发标记
- 标记阶段与应用线程一同进行,进一步降低停顿时间(G1和ZGC)
分代标记
- 对象分代后,新生代和老年代按照不同的标记策略,提升性能
加餐知识 三色标记法
是垃圾回收(GC)中的一种对象标记算法,常用于并发垃圾收集器(如 G1、ZGC)中。它通过将对象分为三种颜色(白色、灰色和黑色)来表示其标记状态,能够有效地支持并发标记和增量标记,从而减少 GC 暂停时间。
三色标记法的基本概念
白色(White)
- 未被标记到的对象
- 在标记阶段开始时,所有对象都是白色
- 最终未被标记为其他颜色的白色对象会被回收
灰色(Gray)
- 已经被访问,但其引用的子对象是尚未被完全处理的对象
- 灰色对象表示“正在处理”的状态,需要继续递归处理其引用
黑色(Black)
- 已经被访问,且其引用的子对象全部被处理完毕的对象
- 黑色对象表示“完全标记完成”,不会再重新扫描
三色标记的过程
初始化阶段:
- 所有对象默认为白色
- 将从GC Roots可达的对象放入灰色集合,表示需要处理
标记阶段
- 从灰色集合中取出一个对象,将其标记为黑色
遍历该对象的所有引用:
- 如果引用的对象是白色,将其标记为灰色,表示需要处理
- 如果引用的对象是灰色和黑色,则无需操作
- 重复该过程,直到灰色集合为空
清理阶段
- 任何仍然是白色的对象都不可达,标记为垃圾,可以被回收。
三色核心原则
三色不变性
在标记过程中,三种颜色的对象之间关系遵循不变量:
- 黑色对象不能引用白色对象。
- 如果一个灰色对象引用了白色对象,那么该白色对象最终会被处理。
强三色不变性
- 黑色对象不能直接或间接引用白色对象。
- 确保所有白色对象都无法通过黑色对象再次可达。
弱三色不变性
- 黑色对象可以直接引用白色对象,但灰色对象必须先处理所有引用。
- 更适合并发场景,因为更宽松的约束允许应用线程和标记线程同时运行。
并发标记中的问题
在并发场景下,应用线程和垃圾收集器同时运行可能导致 漏标记问题(对象被误认为不可达),需要特殊处理。
问题:漂白问题(对象被错误回收)
- 如果应用线程在标记阶段将某个黑色对象的引用指向一个白色对象,那么该白色对象可能会跳过标记,最终被错误回收。
解决方案:写屏障
为了避免上述问题,引入写屏障机制。写屏障通过拦截对象引用的修改操作,确保引用关系的改变不会破坏三色不变性。
两种写屏障策略:
增灰策略:
- 当应用线程将一个白色对象引用赋值给黑色对象时,将该白色对象重新标记为灰色。
- 确保其能被继续处理。
增黑策略:
- 禁止应用线程直接修改黑色对象,使其引用白色对象,直到标记完成。
- 这种方式简单但可能降低并发效率。
三色标记法的优缺点
优点:
支持并发标记:
- 标记线程和应用线程可以同时运行,减少停顿时间。
适合增量标记:
- 标记过程可以分为多个小步骤执行,避免长时间的暂停。
理论清晰:
- 基于可达性分析和三色不变性,逻辑明确,易于扩展。
缺点:
需要写屏障:
- 为了维护三色不变性,需要引入写屏障,增加了实现复杂度。
可能产生额外的开销:
- 写屏障和增灰策略会带来额外的 CPU 负载。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。