分代收集理论

分代收集理论建立在两个假说之上:

1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。

2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

正是由于这两个假说,所以多款垃圾收集器都有一个设计原则:Java堆应该划分不同的区域,然后根据对象的年龄(对象熬过垃圾手机过程的次数)分配到不同的区域存储。

把那些朝生夕灭的对象放在一起存储,那么虚拟机只需要关注如果保留少量存活的对象即可,而不是大量的去进行标记。

把那些不容易被清除的对象放在一起,那么虚拟机就可以不那么频繁的去清理这部分的对象。

由于区域的划分,所以存在了Minor GC(指目标只是新生代的垃圾回收,也叫Young GC)Major GC(指的是目标老年代的垃圾回收,也叫Old GC。目前只有CMS收集器有。)Full GC(收集整个Java堆和方法区的垃圾收集)这样回收类型的划分。而且也根据不同区域对象存亡的特征,发展除了标记-复制算法标记-清除算法标记-整理算法。Mixed GC(混合收集)指目标是整个新生代和部分老年代的垃圾回收,目前只有G1垃圾回收有。

一个最基本的分代是分为新生代和老年代,在这种分代下,如果要进行一次针对新生代的垃圾收集,存在一种情况就是新生代的对象引用了老年代的对象,那么就不得不遍历整个老年代。所以引出了第三个假说:

3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

根据这条假说,我们可以在新生代建立一个叫做记忆集的数据结构,该结构把老年代划分为若干块,标识出老年代的哪一块内存存在跨代引用。此后发生Major GC时,只需要遍历存在跨代引用那些小块里面的对象。

标记-清除算法

正如名字,如它的名字一样,算法分为标记和清除两个阶段。首先标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象。也可以反过来,标记存活的,然后收集所有没有标记的。

存在的问题:

第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法

为了解决标记-清除算法面对大量对象效率低的问题,产生了一种半区复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

该算法存在的问题:

1、如果大多数对象都是活着的,那么意味着会有大量的对象需要复制,会浪费很多内存开销。

2、原来一整块内存现在只能用其中的一半了。

Appel式回收

由于标记-复制算法大多数用在针对新生代的垃圾回收,大多数对象活不过一轮收集,所以并不需要按照1:1的比例进行空间划分,对其划分进行优化后,产生了一种Appel式的回收。

具体做法是把新生代分为一块较大的Eden空间两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是说最多有1块Survivor没有,也就是10%。

Appel式回收时,如果那一块Survivor区不够用于存储还活着的对象,它会向老年区“借用”一部分区域。说白了,这些多余的对象会在分配担保机制下直接进入老年代。

标记-整理算法

该算法主要针对老年代的回收,因为标记-复制算法要不需要浪费一半空间,要不就需要额外的空间进行担保,以防止所有对象都是存活的情况。

标记-整理算法的标记过程与标记-清理一样,但是标记完不是直接清除标记对象,而是把活着的对象向内存空间一端移动,然后直接清理掉边界以外的数据。具体如下图:

image-20230405195521861

优点与缺点

标记完后,如果移动存活的对象,移动对象并且更新这些对象的引用是一项繁重的操作,会带来性能影响。而且在移动的过程中还需要暂停用于程序,人们把暂停称为“stop the world”。

但是如果不移动对象,那么就会导致空间是散列开的,导致很严重的碎片化问题,就只能依赖于更复杂的内存分配器以及内存访问器来解决,譬如通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现的)。

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

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。

参考

《深入理解Java虚拟机》