第三章 垃圾收集器与内存分配策略 | part 2

45 阅读5分钟

垃圾收集算法

分代收集理论

分代收集是一套符合大多数程序运行实际情况的经验法则,它基于两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的;
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡。

这两个假说奠定了多款垃圾收集器的设计原则:将 Java 堆划分不同区域,并将对象依据其年龄分配到不同的区域中存储。

把分代收集理论在 JVM 中具体实践,设计者一般至少把堆划分成新生代和老年代两个区域:新生代中存放朝生夕灭的对象,而熬过了多次垃圾回收过程的对象则移动到老年代中。这样做的好处是,对于新生代,只需要去标记少量的存活对象,以较低代价回收到大量的空间(Minor GC);对于老年代,虚拟机只需要哦以较低的频率来回收(Major GC),这样就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

假设现在要进行一次局限于新生代的 Minor GC,但新生代中的对象有可能被老年代引用(新生代的对象晋升到老年代、直接在老年代分配对象、老年代对象的引用关系发生变更等),为了找出该区域的存活对象,不得不在固定的 GC Roots 之外,再额外遍历整个老年代中所有对象来确保可达性分析的正确性,这无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

依据此假说,我们就不应再为了少量的跨代引用去扫描整个老年代,只需在新生代建立一个全局的数据结构 —— 记忆集(Remembered Set),这个结构把老年代分成若干块,标志出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有包含了跨代引用的小块内存的对象才会被加入到 GC Roots 中。

  • Minor GC/Young GC:指目标只是新生代的垃圾收集。
  • Major GC/Old GC:指目标只是老年代的垃圾收集,仅 CMS 存在。
  • Mixed GC:指目标是整个新生代和部分老年代的垃圾收集,仅 G1 存在。
  • Full GC:指目标是整个 Java 堆和方法区的垃圾收集。

标记-清除算法

该算法分成 “标记” 和 “清除” 两个阶段:首先标记要回收的对象,然后统一清除被标记的对象(或者反过来)。

image.png

这个方法有两个明显的缺点:

  1. 执行效率不稳定,如果堆中包含大量对象,且其中大部分需要被回收,会导致标记和清除两个过程的执行效率都随对象数量增长而降低。
  2. 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法

该算法将可用内存按容量划分为大小相等的两块 A 和 B,每次只使用其中一块。当块 A 用完时,就将还存活的对象复制到块 B,然后清除块 A。如果内存中多数对象是存活的,会产生大量的复制开销,不过新生代朝生夕死的特点似乎正好适合这种算法。针对 “新生代 98% 的对象熬不过第一轮收集” 这个结论,Andrew Appel 提出将新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和 其中一块 Survivor。当发生垃圾收集时,将 Eden 和 Survivor 中存活的对象一次性复制到另一块 Survivor 空间中,并清除 Eden 和已使用的 Survivor 区域。HotSpot 虚拟机默认三者大小比是 8:1:1

那如果 Survivor 空间不足以容纳一次 Minor GC 后存活的对象,该怎么办呢?这时候就要空间担保机制发挥作用了,具体将在之后详细讲解。

image.png

这个方法缺点也有两个:

  1. 如果内存中多数对象是存活的,会产生大量的复制开销。
  2. 可用内存缩小为了原来的一半。

标记-整理算法

前面有提到,标记-复制算法适合新生代的收集,那老年代应该采用什么样的策略呢?这一小节的标记-整理算法正好适合老年代选用。

该算法标记过程和标记-清除相同,但后续步骤是让所有存活对象都向内存空间的一端移动,即 “整理”,然后直接清理掉边界之外的内存。

image.png

标记-整理标记-清除相比,本质上的差异就是是否移动存活对象。如果移动,尤其是在老年代这种每次回收都有大量对象存活的区域,会造成很大的负担,较长时间的 Stop The World,较高的延迟;如果不移动,则会产生大量内存碎片,使得内存分配更复杂,降低吞吐量。因此两者都有利有弊。事实上,HotSpot 虚拟机中关注吞吐量的 Parallel Scavenge 收集器是基于标记-整理的,而关注低延迟的 CMS 收集器则是基于标记-清除的(当内存碎片过多时触发一次标记-整理)。


本节内容比较少,也比较简单,这些思想是如何落实到实际的,将在下节揭晓!