垃圾标记

是垃圾回收的第一阶段,也是垃圾回收算法中关键的一步。它的主要目的是通过遍历内存中存活的对象,来标记哪些对象仍是可达的(即活跃的),哪些对象是不可达的(既可以被垃圾回收)。

标记的结果,将作为下一步(清理,压缩)的依据,确定哪些对象需要被释放或者移动

垃圾回收策略

  1. 引用计数法

原理:

  • 每个对象维护一个 引用计数器,用于记录该对象被引用的次数。
  • 当对象被创建或被引用时,计数器增加。
  • 当引用被移除或销毁时,计数器减少。
  • 如果计数器为零,说明该对象不可达,可以被回收。

实现流程:

  • 初始化时,计数器设为 0。
  • 每当有新的引用指向该对象时,计数器 +1。
  • 每当引用消失时,计数器 -1。
  • 如果计数器为 0,该对象会被立即回收。

缺点:

  • 循环引用问题:

    • 如果两个或多个对象相互引用,但没有其他外部对象引用它们,则它们的计数器始终大于零,导致无法被回收。
    • 例如:
class Node {
    Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
  • 维护开销大:

    • 每次引用变化都需要更新计数器,可能会影响性能。
  1. 根搜索法

原理:

  • 从一组称为 GC Roots 的根对象开始,通过引用关系遍历对象图。
  • 如果某个对象可以通过 GC Roots 找到,则认为它是可达的;否则认为它是不可达的,可以被回收。

实现流程:

  • 从 GC Roots 出发,标记所有可以访问到的对象。
  • 遍历整个堆,将未被标记的对象视为垃圾,进行回收。

优点:

  • 无循环引用问题:

    • 可达性分析仅关注对象是否能从根集合到达,不会因循环引用无法回收。
  • 适用于复杂的引用关系:

    • 能处理对象图中存在大量交叉引用的复杂场景。
  • 分代垃圾回收的基础:

    • 新生代、老年代的分代策略都基于此方法。

缺点:

  • 暂停应用线程:

    • 传统的垃圾收集器需要暂停所有应用线程(Stop-the-World),执行可达性分析,可能导致停顿时间较长。
  • 实现复杂:

    • 需要额外的辅助数据结构和更复杂的遍历逻辑。

分代回收策略

  1. 新生代 GC(Minor GC)
  • 触发条件:

    • 新生代的 Eden 区满时触发。
  • 回收策略:

    • 采用 复制算法:
    • 将 Eden 区和一个 Survivor 区中的存活对象复制到另一个 Survivor 区或老年代。
    • 减少内存碎片问题,适合对象存活时间短的特点。
  • 特点:

    • 回收速度快,代价低。
  1. 老年代 GC(Major GC / Full GC)
  • 触发条件:

    • 老年代内存不足时触发。
  • 回收策略:

    • 通常采用 标记-清除算法 或 标记-整理算法:

      • 标记-清除算法:标记存活对象,清除无用对象,但可能会产生碎片。
      • 标记-整理算法:在标记存活对象后,将存活对象移动到一侧,整理内存,避免碎片。
    • 特点:

      • 回收速度较慢,代价高,可能会导致应用长时间停顿。

垃圾回收器

  1. Serial GC
  • 特点:

    • 单线程工作
    • 适用于单核环境和小堆内存(100MB)
    • 新生代使用复制算法,老年代使用标记-整理算法
  • 优点:

    • 实现简单,适合小型应用
  • 缺点

    • 垃圾回收期间会停止所有应用线程(STW),延迟较高
  • 适用环境

    • 单线程,内存较小的应用
  1. Parallel GC
  • 特点:

    • 多线程执行垃圾回收,提升回收速度
    • 注意吞吐量(应用运行时间与垃圾回收时间的比值)
    • 新生代使用复制算法,老年代使用标记-整理算法
  • 优点:

    • 吞吐量高,适合多核CPU
  • 缺点:

    • STW时间比较长,不适合低延迟的场景
  • 使用场景:

    • 后台计算型任务,大批量数据处理应用
  1. CMS GC(Concurrent Mark-Sweep)
  • 特点

    • 专注于低延迟,适合对响应时间要求高的应用
    • 老年代使用标记-清理算法,新生代使用复制算法
    • 老年代回收分为以下阶段

      • 初始阶段:标记GC Roots可达的对象(短暂暂停)
      • 并发标记:多线程遍历对象图
      • 重新标记:修正并发标记阶段的变动对象(短暂暂停
      • 并发清楚:清楚不可达对象
  • 优点

    • 停顿时间短,适合交互型应用。
  • 缺点

    • 容易产生内存碎片
    • 并发清理阶段可能导致系统吞吐量下降
  • 使用场景

    • 低延迟、高响应的应用,如Web服务器
  1. G1 GC(Garbage First)
  • 特点:

    • 通过将堆划分为多个区域,以区域为单位回收
    • 同时支持新生代和老年代的回收(混合回收)
    • 停顿可控:允许用户设置最大停顿时间目标
  • 回收过程

    • 初始标记:标记 GC Roots 直接可达的对象。
    • 并发标记:遍历堆中的对象图,标记存活对象。
    • 筛选回收:按回收收益优先级回收部分区域。
    • 清理:整理内存,释放区域。
  • 优点:

    • 减少老年代碎片。
    • 可控的停顿时间。
  • 缺点:

    • 相比 CMS,初期调优难度较高。
  • 使用场景:

    • 大堆内存、多核 CPU 的低延迟场景。
  1. ZGC(Z Garbage Collector)
  • 特点:

    • 超低延迟垃圾回收器,目标是将停顿时间控制在10ms以下
    • 使用染色指针标记对象状态
    • 回收过程

      • 并发标记:找到存活对象
      • 并发转移:将存活对象复制到新区域
      • 并发清除:清除垃圾对象
    • 支持非常大的堆内存(TB 级别)
  • 优点:

    • 停顿时间极低,适合实时性要求高的场景
  • 缺点:

    • 初期内存占用比较高
    • 仅支持64位系统
  • 使用场景:

    • 超低延迟需求的应用,如金融交易系统
  1. Shenandoah GC
  • 特点:

    • 和ZGC类似,目标是低延迟,减少STW时间
    • 通过并发整理老年代对象,避免碎片化
  • 优点:

    • 和 G1 相比,停顿时间更短
  • 缺点:

    • 内存开销高
  • 使用场景:

    • 延迟敏感的服务端应用

垃圾标记的过程

  1. 从根集合(GC Roots)出发:
  2. 垃圾标记阶段以一组成为GC Roots的引用集合为起点。
  3. 常见的GC Roots包括:

    • Java栈中的局部变量
    • 方法区的静态变量和常量
    • 本地方法栈中的JNI引用
  • 通过引用关系遍历对象图:

    • 以GC Roots为起点,通过深度优先或者广度优先的方式遍历所有可达的对象。
    • 遍历到的对象会被标记为“存活”状态
  • 标记不可达对象:

    • 未被访问到的对象会被认为是垃圾,标记为“不可达”

垃圾标记的方式

垃圾标记的具体实现方式可能因垃圾回收算法不同而异。常见的标记方式有:

  1. 标记-清除算法(Mark-Sweep)

    • 标记阶段:从GC Roots出发,遍历并标记所有存活的对象
    • 清除阶段:扫描堆内存,清理未被标记的对象
    • 缺点:

      • 标记和清理后会产生很多的碎片
  2. 标记-复制算法(Mark-Copy)

    • 将可达对象复制到新的区域,未被复制的对象被认为是垃圾
    • 优点:

      • 避免内存碎片,但需要额外的内存空间
  3. 标记-整理算法(Mark-Compact)

    • 标记阶段:标记所有存活的对象
    • 整理阶段:将存活的对象压到堆的一端,清理无用对象,释放空间
    • 优点:

      • 解决了内存碎片的问题
  4. 三色标记法(多线程GC的经典方式)

    • 白色:尚未访问的对象(默认状态,潜在垃圾)
    • 灰色:已访问但未处理完引用的对象
    • 黑色:已访问且其引用已处理完的对象
    • 标记完成后,白色对象即为垃圾,可以被回收

GC Roots的作用

  • GC Roots是垃圾标记的起点,决定了哪些对象是根可达的
  • 可通过以下途径访问到的对象会被视为存活:

    • 栈中的局部变量
    • 静态属性引用的对象
    • 常量引用的对象
    • JNI(本地方法接口)中引用的对象

标记技术的优化

现代 GC 使用了多种技术优化垃圾标记的性能:

  1. 增量标记

    • 讲标记过程拆分成多个小步骤,减少GC的事件
  2. 并发标记

    • 标记阶段与应用线程一同进行,进一步降低停顿时间(G1和ZGC)
  3. 分代标记

    • 对象分代后,新生代和老年代按照不同的标记策略,提升性能

加餐知识 三色标记法

是垃圾回收(GC)中的一种对象标记算法,常用于并发垃圾收集器(如 G1、ZGC)中。它通过将对象分为三种颜色(白色、灰色和黑色)来表示其标记状态,能够有效地支持并发标记和增量标记,从而减少 GC 暂停时间。

三色标记法的基本概念

  1. 白色(White)

    • 未被标记到的对象
    • 在标记阶段开始时,所有对象都是白色
    • 最终未被标记为其他颜色的白色对象会被回收
  2. 灰色(Gray)

    • 已经被访问,但其引用的子对象是尚未被完全处理的对象
    • 灰色对象表示“正在处理”的状态,需要继续递归处理其引用
  3. 黑色(Black)

    • 已经被访问,且其引用的子对象全部被处理完毕的对象
    • 黑色对象表示“完全标记完成”,不会再重新扫描

三色标记的过程

  1. 初始化阶段:

    • 所有对象默认为白色
    • 将从GC Roots可达的对象放入灰色集合,表示需要处理
  2. 标记阶段

    • 从灰色集合中取出一个对象,将其标记为黑色
    • 遍历该对象的所有引用:

      • 如果引用的对象是白色,将其标记为灰色,表示需要处理
      • 如果引用的对象是灰色和黑色,则无需操作
    • 重复该过程,直到灰色集合为空
  3. 清理阶段

    • 任何仍然是白色的对象都不可达,标记为垃圾,可以被回收。

三色核心原则

  1. 三色不变性

    • 在标记过程中,三种颜色的对象之间关系遵循不变量:

      • 黑色对象不能引用白色对象。
      • 如果一个灰色对象引用了白色对象,那么该白色对象最终会被处理。
  2. 强三色不变性

    • 黑色对象不能直接或间接引用白色对象。
    • 确保所有白色对象都无法通过黑色对象再次可达。
  3. 弱三色不变性

    • 黑色对象可以直接引用白色对象,但灰色对象必须先处理所有引用。
    • 更适合并发场景,因为更宽松的约束允许应用线程和标记线程同时运行。

并发标记中的问题

在并发场景下,应用线程和垃圾收集器同时运行可能导致 漏标记问题(对象被误认为不可达),需要特殊处理。

问题:漂白问题(对象被错误回收)

  • 如果应用线程在标记阶段将某个黑色对象的引用指向一个白色对象,那么该白色对象可能会跳过标记,最终被错误回收。

解决方案:写屏障

为了避免上述问题,引入写屏障机制。写屏障通过拦截对象引用的修改操作,确保引用关系的改变不会破坏三色不变性。

两种写屏障策略:

  1. 增灰策略:

    • 当应用线程将一个白色对象引用赋值给黑色对象时,将该白色对象重新标记为灰色。
    • 确保其能被继续处理。
  2. 增黑策略:

    • 禁止应用线程直接修改黑色对象,使其引用白色对象,直到标记完成。
    • 这种方式简单但可能降低并发效率。

三色标记法的优缺点

优点:

  1. 支持并发标记:

    • 标记线程和应用线程可以同时运行,减少停顿时间。
  2. 适合增量标记:

    • 标记过程可以分为多个小步骤执行,避免长时间的暂停。
  3. 理论清晰:

    • 基于可达性分析和三色不变性,逻辑明确,易于扩展。

缺点:

  1. 需要写屏障:

    • 为了维护三色不变性,需要引入写屏障,增加了实现复杂度。
  2. 可能产生额外的开销:

    • 写屏障和增灰策略会带来额外的 CPU 负载。

爱跑步的猕猴桃
1 声望0 粉丝