垃圾回收算法介绍

垃圾回收算法是垃圾回收器用来识别和回收不再使用的内存(即“垃圾”)的具体策略和方法。这些算法通常关注两个核心问题:一是如何判断哪些对象是垃圾,二是如何清理这些垃圾对象所占用的内存空间。

  1. 引用计数法(Reference Counting):

    • 原理:为每个对象维护一个引用计数器。当一个对象被引用时,其计数器加1;当引用失效(例如,引用变量被赋为null,或超出作用域)时,其计数器减1。当一个对象的引用计数器变为0时,表示该对象不再被任何地方引用,可以被回收。
    • 优点:
      • 实现简单。
      • 对象可被回收的时机比较明确,一旦计数器为0就可以立即回收(或者加入待回收列表),可以减少程序在垃圾回收时可能出现的长时间停顿。
    • 缺点:
      • 需要额外的空间存储计数器。
      • 计数器的增减操作会带来一定的开销。
      • 最致命的缺点是无法处理循环引用问题。如果两个或多个对象相互引用,但没有外部引用指向它们,它们的引用计数器永远不会为0,即使它们实际上已经成为垃圾,也无法被回收,从而导致内存泄漏。
    • 应用:虽然主流JVM(如HotSpot)不使用此算法作为主要的垃圾判断手段,但某些语言(如Python的部分实现)或特定场景下仍有应用。
  2. 标记-清除算法(Mark-Sweep):

    • 这是基于可达性分析的垃圾回收算法。
    • 原理:分为两个阶段: a. 标记(Mark)阶段:从GC Roots(如虚拟机栈中的引用、静态变量等)开始,遍历所有可达对象,并对这些可达对象进行标记(通常是在对象头中设置一个标记位)。 b. 清除(Sweep)阶段:遍历整个堆内存,回收所有未被标记的对象(即不可达对象),清除它们所占用的内存空间。
    • 优点:
      • 解决了循环引用的问题,因为循环引用的对象组如果从GC Roots不可达,它们将不会被标记。
      • 实现相对简单。
    • 缺点:
      • 效率问题:标记和清除两个过程的效率都不算高,尤其是在堆中对象数量庞大时。
      • 空间碎片问题:清除之后会产生大量不连续的内存碎片。如果后续需要分配较大的对象,可能因为找不到足够大的连续内存空间而不得不提前触发下一次垃圾收集。
  3. 标记-复制算法(Mark-Copy,通常简称复制算法):

    • 原理:将可用的内存空间划分为大小相等的两块,比如From空间和To空间,每次只使用其中一块(比如From空间)。当From空间用完需要进行垃圾回收时,算法会遍历From空间,将所有存活的对象(通过可达性分析标记出来的)复制到To空间中,并且是紧凑排列的。复制完成后,From空间中的所有对象(包括原来的存活对象和垃圾对象)都可以被视为垃圾,直接一次性清空From空间。然后,From空间和To空间的角色互换,下一次垃圾回收时,从新的From空间(原来的To空间)复制到新的To空间(原来的From空间)。
    • 优点:
      • 实现简单,运行高效(特别是存活对象较少时,复制开销小)。
      • 不会产生内存碎片,每次回收后都能得到一块连续的可用内存空间。
    • 缺点:
      • 内存利用率低:可用内存缩小为实际总内存的一半。
      • 对于存活对象较多的情况,复制操作的开销会比较大,并且需要移动对象,修改引用。
    • 应用:非常适合新生代(Young Generation)的垃圾回收,因为新生代中的对象大多“朝生夕死”,每次GC时存活对象很少。JVM中的新生代通常采用这种算法的变种(如Appel式回收,将新生代分为一个较大的Eden区和两个较小的Survivor区)。
  4. 标记-整理算法(Mark-Compact):

    • 原理:结合了标记-清除和复制算法的优点。 a. 标记(Mark)阶段:与标记-清除算法一样,从GC Roots开始标记所有存活对象。 b. 整理(Compact)阶段:不是直接清除未标记对象,而是将所有存活的对象向内存空间的一端移动,并按顺序紧密排列。移动完成后,直接清理掉边界以外的内存区域。
    • 优点:
      • 解决了标记-清除算法的内存碎片问题,回收后内存是规整的。
      • 避免了复制算法那样牺牲一半内存空间的代价。
    • 缺点:
      • 效率问题:移动对象是一个相对耗时的操作,需要更新所有指向这些被移动对象的引用,因此其执行效率通常低于复制算法(尤其是在存活对象较多时)。
      • 停顿时间可能较长:因为涉及到对象的移动和引用的更新。
    • 应用:通常用于老年代(Old Generation)的垃圾回收,因为老年代中的对象存活率较高,不适合使用复制算法,而标记-整理能有效处理内存碎片。
  5. 分代收集算法(Generational Collection):

    • 这并非一种独立的回收算法,而是一种基于对象生命周期特点的垃圾回收策略。
    • 原理:根据对象存活时间的不同,将Java堆划分为不同的区域(代),如新生代和老年代。然后针对不同代的特点选用最适合的垃圾回收算法。
      • 新生代:对象生命周期短,大量对象在GC时被回收。通常采用复制算法。
      • 老年代:对象生命周期长,存活率高。通常采用标记-清除或标记-整理算法。
    • 这种策略可以显著提高垃圾回收的效率,因为大部分GC可以集中在对象变化频繁的新生代,而不需要每次都扫描整个堆。

这些基础算法是构建复杂垃圾回收器的基石。现代JVM中的垃圾回收器(如Serial, Parallel Scavenge, CMS, G1, ZGC, Shenandoah等)往往是这些基础算法的组合、优化或变种,并结合了并发、并行等技术,以满足不同应用场景对吞吐量、停顿时间等性能指标的需求。