分代收集理论

分代收集名为理论,实质是一套符合大多数程序员运行实际情况的经验法则,它建立在两个分代假说之上:

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

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则

  • 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

在 Java 堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记 - 复制算法”“标记 - 清除算法”“标记 - 整理算法”等针对性的垃圾收集算法。

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;目前只有 CMS 收集器会有单独收集老年代的行为。
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。目前只有 G1 收集器会有这个行为

整堆收集 (Full GC):收集整个 Java 堆和方法区。

把分代收集理论具体放在现在的商用 Java 虚拟机里,设计者一般至少会把 Java 堆划分为 新生代老年代 两个区域。

跨代引用问题

什么是跨代引用?

对象不是孤立的,对象之间会存在跨代引用。
假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的 GC Roots 之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

如何解决?

分代收集理论的第三条经验法则:

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

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到 GC Roots 进行扫描。

标记 - 清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

它的主要缺点有两个:

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

标记-清除算法.jpeg

标记 - 复制算法

为了解决效率问题,“标记-复制”收集算法出现了。
半区复制”可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
复制算法.png
HotSpot 虚拟机的 Serial、ParNew 等新生代收集器采用“Appel 式回收”策略来设计新生代的内存布局。

Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中任然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代。

标记 - 复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记 - 整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记 - 清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

标记整理算法.png

是否移动对象都存在弊端,移动则回收内存时更复杂,不移动则内存分配时会更复杂。

CMS 收集器面临空间碎片过多时采用的处理办法:让虚拟机平时多数时间都采用标记 - 清除算法,暂时容忍内存碎片的存在,直至内存空间的碎片化程度已经大到影响对象分配时,再采用标记 - 整理算法收集一次,以获得规整的内存空间。

面试题

JVM 什么时候会触发垃圾回收?

GC 主要有两种类型:Minor GC和 Full GC,Minor GC 是对新生代进行垃圾回收, Full GC 是对整个堆进行垃圾回收。

Minor GC 的触发条件:
一般情况下,当新对象生成,并且在Eden区满时,就会触发 Minor GC,对 Eden区域进行 GC, 清除非存活对象,并且把尚且存活的对象移动到 Survivor区。然后整理 Survivor的两个区。

Full GC 的触发条件:

  • 当 Eden区和 From Survivor区满时
  • 年老代空间不足
  • 方法区空间不足
  • System.gc()被显示调用
  • 发生 Minor GC 并且空间担保失败时