JVM - 垃圾收集算法

JVM 解密 —— 垃圾收集算法

通过可达性分析,JVM 已经知道了哪些是垃圾。现在的问题是,如何高效地回收这些垃圾所占用的空间?这就是垃圾收集算法要解决的问题。

1. 深度剖析:核心收集算法

1.1 标记-清除算法 (Mark-Sweep)

这是最基础的收集算法,后续的算法都是基于它的思想进行改进的。

  • 执行过程:

    1. 标记 (Marking): 首先,从 GC Roots 开始,遍历所有可达对象,并给它们打上“存活”标记。
    2. 清除 (Sweeping): 再次遍历整个堆,将所有没有被标记为“存活”的对象(即垃圾对象)进行回收,清除其所占用的内存空间。
  • 优点: 实现简单,不需要移动对象。

  • 缺点:

    1. 效率问题: 需要进行两次遍历(一次标记,一次清除),效率不高。
    2. 空间碎片问题: 清除后会产生大量不连续的内存碎片。如果后续需要分配一个较大的对象,可能会因为找不到足够大的连续内存空间,而不得不提前触发又一次垃圾收集动作。
  • 生活比喻: 就像一个停车场管理员清理乱停车。

    • 标记: 管理员拿出登记表,核对哪些车是合法登记的,在车上贴一个“已登记”的条子。
    • 清除: 管理员再次从头走到尾,看到所有没贴条子的车,就直接叫拖车把它们拖走。
    • 碎片: 车被拖走后,原地会留下一个个零散的空车位,但可能没有一个足够长的、能停下一辆加长林肯的连续空位。

1.2 标记-复制算法 (Mark-Copy)

为了解决“标记-清除”的效率和碎片问题,复制算法应运而生。它常用于新生代的垃圾回收。

  • 执行过程: 它将可用的内存按容量划分为大小相等的两块,比如 A 区和 B 区,每次只使用其中的一块(比如 A 区)。

    1. 当 A 区的内存用完了,就触发 GC。
    2. 将 A 区中所有存活的对象复制到另一块完全未被使用的 B 区中。
    3. 一次性地清空整个 A 区。
    4. 下次就在 B 区分配内存,A 区和 B 区的角色互换。
  • 优点:

    1. 效率高: 只需遍历一次存活对象,然后直接复制和移动内存指针即可,无需遍历垃圾对象。
    2. 无内存碎片: 每次都是将存活对象整齐地复制到另一边,所以不会产生碎片。
  • 缺点:

    1. 空间浪费: 需要将总内存空间“腰斩”,可使用空间只有原来的一半,代价高昂。
  • 生活比喻: 就像整理两个房间

    • 你总是在 A 房间里生活、扔垃圾。当 A 房间乱到无法下脚时,你不会在原地打扫。
    • 你会把 A 房间里所有你还想要的、有用的东西(存活对象)全部搬到一尘不染的 B 房间里,并把它们整齐地码好。
    • 然后,你叫来一个清洁队,把整个 A 房间夷为平地(清空),里面所有的垃圾自然就都没了。

1.3 标记-整理算法 (Mark-Compact)

复制算法在对象存活率较高时(比如老年代)会进行大量的复制操作,效率变低,且空间浪费的问题依然存在。因此,标记-整理算法被提了出来。

  • 执行过程:

    1. 标记 (Marking): 过程与“标记-清除”算法一样,先标记出所有存活对象。
    2. 整理 (Compacting): 不是直接对未标记对象进行清理,而是将所有存活的对象都向内存空间的一端移动,并更新所有引用这些对象的指针。
    3. 清理掉边界以外的内存。
  • 优点:

    1. 无内存碎片: 清理后,内存是连续的。
    2. 空间利用率高: 不需要像复制算法那样牺牲一半的空间。
  • 缺点:

    1. 效率较低: 不仅要标记存活对象,还要移动所有存活对象,并更新它们的引用地址,是一个相对耗时的操作。
  • 生活比喻: 就像电影院散场后的清扫

    • 标记: 清洁工先看哪些座位上还留有观众的物品(存活对象)。
    • 整理: 清洁工并不会一个一个地去捡垃圾,而是把所有有用的物品都收集起来,统一移动到影院的前排座位上放好。
    • 清扫: 然后,他可以非常高效地宣布:“从第 5 排往后,所有的座位都可以一次性清空了!”

2. 核心思想:分代收集 (Generational Collection)

现代商业虚拟机大都采用“分代收集”思想,它并不是一种具体的算法,而是一种策略,它将以上三种算法进行组合,以达到最优的回收效果。

  • 理论基础: 基于一个重要的观察——绝大部分 Java 对象的生命周期都非常短暂,“朝生夕死”。而只有少数对象能活很长时间。

  • 策略: 根据这个特点,JVM 的堆被划分为两个主要区域:

    1. 新生代 (Young Generation):

      • 特点: 存放生命周期短的对象。每次垃圾回收时,都有大量的对象被发现是“垃圾”。

      • 适用算法: 因为存活对象少,所以采用标记-复制算法。只需复制少量存活对象,就可以清空整个区域,效率极高。

      • 内部结构与晋升过程: 新生代内部通常采用一种名为“Appel式回收”的策略,它将新生代细分为一个 Eden 区和两个大小相等的 Survivor 区(通常称为 From 和 To)。一个新对象从诞生到晋升老年代的完整旅程如下:

        1. 出生在 Eden: 绝大部分新对象在 Eden 区分配内存。
        2. 第一次 Minor GC: 当 Eden 区满了,触发 Minor GC。Eden 区的存活对象被复制到其中一个空的 Survivor 区(比如 To 区),并且对象的年龄计数器设为 1。然后清空 Eden 区。
        3. 在 Survivor 区流转: 当 Eden 区再次满时,触发又一次 Minor GC。这次会清理 Eden 区和当前有对象的那个 Survivor 区(From 区)。将这两个区域中所有存活的对象,一起复制到另一个空的 Survivor 区(To 区)。所有被复制的对象的年龄都加 1
        4. 晋升老年代: 对象在 Survivor 区每“熬过”一次 Minor GC,年龄就加 1。当它的年龄达到一个晋升阈值MaxTenuringThreshold,默认是 15),在下一次 GC 时,它就会被直接移动到老年代,而不再是在 Survivor 区之间复制。
        5. 特殊情况: 如果一个对象过大,新生代无法容纳,它可能会被直接分配到老年代
      • 生活比喻:从幼儿园到养老院

        • Java 堆: 整个社会
        • 新生代: 社会里的教育系统(幼儿园到大学)。
        • 老年代: 社会上的工作和生活区域
        1. 出生在 Eden 区 (幼儿园): 绝大部分新生儿(新对象)都在幼儿园(Eden 区)里快乐地玩耍。
        2. 第一次 Minor GC (上小学): 幼儿园毕业时进行一次“大清洗”(Minor GC)。大部分被“淘汰”(回收),只有少数“好孩子”(存活对象)进入小学一年级(一个 Survivor 区),他们的“年龄”变为 1。
        3. 在 Survivor 区流转 (升年级): 每次升级(又一次 Minor GC),都会把幼儿园里新的好孩子当前年级里仍然优秀的学生,一起送到下一个年级(另一个 Survivor 区)。学生的“年龄”都加 1。
        4. 晋升到老年代 (进入社会): 当一个学生的“年龄”达到阈值(比如 15 岁),学校就认为他“成年”了。下次升级时,他将直接“毕业”,进入社会(被移动到老年代)
        5. 大对象直接进入老年代 (天才少年): 个别“天才少年”(大对象),会被直接保送到社会(老年代)去工作。
    2. 老年代 (Old Generation):

      • 特点: 存放生命周期长的对象(从新生代中“熬”过来的对象)。每次垃圾回收时,只有少量对象是“垃圾”。
      • 适用算法: 因为存活对象多,如果用复制算法,成本太高。因此采用标记-清除标记-整理算法。
    3. 优点:充分利用不同代对象的特点,优化了整个 GC 过程,提高了回收效率和内存利用率。
      缺点算法思想更复杂,需要考虑代际之间的引用关系(例如新生代对象引用老年代对象

posted @ 2026-01-21 16:06  我是刘瘦瘦  阅读(0)  评论(0)    收藏  举报