JVM - 垃圾收集算法
JVM 解密 —— 垃圾收集算法
通过可达性分析,JVM 已经知道了哪些是垃圾。现在的问题是,如何高效地回收这些垃圾所占用的空间?这就是垃圾收集算法要解决的问题。
1. 深度剖析:核心收集算法
1.1 标记-清除算法 (Mark-Sweep)
这是最基础的收集算法,后续的算法都是基于它的思想进行改进的。
-
执行过程:
- 标记 (Marking): 首先,从 GC Roots 开始,遍历所有可达对象,并给它们打上“存活”标记。
- 清除 (Sweeping): 再次遍历整个堆,将所有没有被标记为“存活”的对象(即垃圾对象)进行回收,清除其所占用的内存空间。
-
优点: 实现简单,不需要移动对象。
-
缺点:
- 效率问题: 需要进行两次遍历(一次标记,一次清除),效率不高。
- 空间碎片问题: 清除后会产生大量不连续的内存碎片。如果后续需要分配一个较大的对象,可能会因为找不到足够大的连续内存空间,而不得不提前触发又一次垃圾收集动作。
-
生活比喻: 就像一个停车场管理员清理乱停车。
- 标记: 管理员拿出登记表,核对哪些车是合法登记的,在车上贴一个“已登记”的条子。
- 清除: 管理员再次从头走到尾,看到所有没贴条子的车,就直接叫拖车把它们拖走。
- 碎片: 车被拖走后,原地会留下一个个零散的空车位,但可能没有一个足够长的、能停下一辆加长林肯的连续空位。
1.2 标记-复制算法 (Mark-Copy)
为了解决“标记-清除”的效率和碎片问题,复制算法应运而生。它常用于新生代的垃圾回收。
-
执行过程: 它将可用的内存按容量划分为大小相等的两块,比如 A 区和 B 区,每次只使用其中的一块(比如 A 区)。
- 当 A 区的内存用完了,就触发 GC。
- 将 A 区中所有存活的对象复制到另一块完全未被使用的 B 区中。
- 一次性地清空整个 A 区。
- 下次就在 B 区分配内存,A 区和 B 区的角色互换。
-
优点:
- 效率高: 只需遍历一次存活对象,然后直接复制和移动内存指针即可,无需遍历垃圾对象。
- 无内存碎片: 每次都是将存活对象整齐地复制到另一边,所以不会产生碎片。
-
缺点:
- 空间浪费: 需要将总内存空间“腰斩”,可使用空间只有原来的一半,代价高昂。
-
生活比喻: 就像整理两个房间。
- 你总是在 A 房间里生活、扔垃圾。当 A 房间乱到无法下脚时,你不会在原地打扫。
- 你会把 A 房间里所有你还想要的、有用的东西(存活对象)全部搬到一尘不染的 B 房间里,并把它们整齐地码好。
- 然后,你叫来一个清洁队,把整个 A 房间夷为平地(清空),里面所有的垃圾自然就都没了。
1.3 标记-整理算法 (Mark-Compact)
复制算法在对象存活率较高时(比如老年代)会进行大量的复制操作,效率变低,且空间浪费的问题依然存在。因此,标记-整理算法被提了出来。
-
执行过程:
- 标记 (Marking): 过程与“标记-清除”算法一样,先标记出所有存活对象。
- 整理 (Compacting): 不是直接对未标记对象进行清理,而是将所有存活的对象都向内存空间的一端移动,并更新所有引用这些对象的指针。
- 清理掉边界以外的内存。
-
优点:
- 无内存碎片: 清理后,内存是连续的。
- 空间利用率高: 不需要像复制算法那样牺牲一半的空间。
-
缺点:
- 效率较低: 不仅要标记存活对象,还要移动所有存活对象,并更新它们的引用地址,是一个相对耗时的操作。
-
生活比喻: 就像电影院散场后的清扫。
- 标记: 清洁工先看哪些座位上还留有观众的物品(存活对象)。
- 整理: 清洁工并不会一个一个地去捡垃圾,而是把所有有用的物品都收集起来,统一移动到影院的前排座位上放好。
- 清扫: 然后,他可以非常高效地宣布:“从第 5 排往后,所有的座位都可以一次性清空了!”
2. 核心思想:分代收集 (Generational Collection)
现代商业虚拟机大都采用“分代收集”思想,它并不是一种具体的算法,而是一种策略,它将以上三种算法进行组合,以达到最优的回收效果。
-
理论基础: 基于一个重要的观察——绝大部分 Java 对象的生命周期都非常短暂,“朝生夕死”。而只有少数对象能活很长时间。
-
策略: 根据这个特点,JVM 的堆被划分为两个主要区域:
-
新生代 (Young Generation):
-
特点: 存放生命周期短的对象。每次垃圾回收时,都有大量的对象被发现是“垃圾”。
-
适用算法: 因为存活对象少,所以采用标记-复制算法。只需复制少量存活对象,就可以清空整个区域,效率极高。
-
内部结构与晋升过程: 新生代内部通常采用一种名为“Appel式回收”的策略,它将新生代细分为一个 Eden 区和两个大小相等的 Survivor 区(通常称为 From 和 To)。一个新对象从诞生到晋升老年代的完整旅程如下:
- 出生在 Eden: 绝大部分新对象在 Eden 区分配内存。
- 第一次 Minor GC: 当 Eden 区满了,触发 Minor GC。Eden 区的存活对象被复制到其中一个空的 Survivor 区(比如 To 区),并且对象的年龄计数器设为 1。然后清空 Eden 区。
- 在 Survivor 区流转: 当 Eden 区再次满时,触发又一次 Minor GC。这次会清理 Eden 区和当前有对象的那个 Survivor 区(From 区)。将这两个区域中所有存活的对象,一起复制到另一个空的 Survivor 区(To 区)。所有被复制的对象的年龄都加 1。
- 晋升老年代: 对象在 Survivor 区每“熬过”一次 Minor GC,年龄就加 1。当它的年龄达到一个晋升阈值(
MaxTenuringThreshold,默认是 15),在下一次 GC 时,它就会被直接移动到老年代,而不再是在 Survivor 区之间复制。 - 特殊情况: 如果一个对象过大,新生代无法容纳,它可能会被直接分配到老年代。
-
生活比喻:从幼儿园到养老院
- Java 堆: 整个社会。
- 新生代: 社会里的教育系统(幼儿园到大学)。
- 老年代: 社会上的工作和生活区域。
- 出生在 Eden 区 (幼儿园): 绝大部分新生儿(新对象)都在幼儿园(Eden 区)里快乐地玩耍。
- 第一次 Minor GC (上小学): 幼儿园毕业时进行一次“大清洗”(Minor GC)。大部分被“淘汰”(回收),只有少数“好孩子”(存活对象)进入小学一年级(一个 Survivor 区),他们的“年龄”变为 1。
- 在 Survivor 区流转 (升年级): 每次升级(又一次 Minor GC),都会把幼儿园里新的好孩子和当前年级里仍然优秀的学生,一起送到下一个年级(另一个 Survivor 区)。学生的“年龄”都加 1。
- 晋升到老年代 (进入社会): 当一个学生的“年龄”达到阈值(比如 15 岁),学校就认为他“成年”了。下次升级时,他将直接“毕业”,进入社会(被移动到老年代)。
- 大对象直接进入老年代 (天才少年): 个别“天才少年”(大对象),会被直接保送到社会(老年代)去工作。
-
-
老年代 (Old Generation):
- 特点: 存放生命周期长的对象(从新生代中“熬”过来的对象)。每次垃圾回收时,只有少量对象是“垃圾”。
- 适用算法: 因为存活对象多,如果用复制算法,成本太高。因此采用标记-清除或标记-整理算法。
-
优点:充分利用不同代对象的特点,优化了整个 GC 过程,提高了回收效率和内存利用率。
缺点算法思想更复杂,需要考虑代际之间的引用关系(例如新生代对象引用老年代对象
-

浙公网安备 33010602011771号