概述:

分代收集理论

标记清除算法,标记复制算法,标记整理算法


从判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(直接垃圾收集),“追踪式垃圾收集”(间接垃圾收集)(JVM使用)

【分代收集理论】

三个假说:

【弱分代假说】绝代多数对象都是朝生夕死的

【强分代假说】熬过越多次垃圾收集过程的对象就越难以消亡

分代假说决定了垃圾收集器的设计原则:

将java堆划分不同的区域,然后将回收对象根据年龄(年龄是对象熬过垃圾收集过程的次数)分配到不同的区域中储存。

【新生代】如果一个区域中大多数对象都是朝生夕死的,把它们集中到一起,每次回收时只关注如何保留少量存活而不是标记大量要被会回收的对象,就能以较低的代价回收到大量的空间

【老年代】如果一个区域中时难以消亡的对象,虚拟机就可以使用较低的频率来回收这个区域。

这样同时兼顾了垃圾收集的时间开销和空间的有效利用。

java堆分区域后,才有了不同回收类型的划分,才有了针对不同区域特点和存亡特征设计不同的垃圾回收算法。

不同回收类型:

部分收集/PartialGC:目标不是回收整个java堆,分为

   MinorGC/young GC/新生代收集

   MajorGC/OldGC/老年代收集

   mixedGC混合收集。

整堆收集/full GC:目标是整个java堆和方法区

【跨代引用假说】跨代引用相对于同代引用仅占极少数
问题:考虑到新生代中的对象可能引用老年代中的对象,反过来一样。

这样在对一个区域进行垃圾回收时就不得不在额外遍历另一个区域确保可达性分析的正确,带来额外的负担。

推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的。

例:如果某个新生代存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代,这时跨代引用消失。

根据这条假说,就不应再为少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨带引用。

【标记-清除算法】

【标记】标记出所有需要(不需要)被回收的对象

【清除】统一回收被标记的对象

是最基础的收集算法,后续收集算法以它为基础加以改进。

 缺点:

【执行效率不稳定】

如果java堆中包含大量对象,其中大部分是需要被回收的,这时就必须进行大量标记和清除的动作。

标记和清除过程的执行效率岁对象数量增长而降低

【内存空间的碎片化问题】

标记清除后会阐述大量不连续的内存碎片,空间碎片太多可能导致程序运行过程中需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

【标记-复制算法】

内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就将还存活着 的对象复制到另外一块上面,然后把已使用的内存空间一次清理掉。

(1)如果内存中大多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象。

(2)每次都是针对整个半区机械能内存回收,分配内存时就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

现代商用的JVM大多优先采用这种算法回收新生代。

【代价】可用内存减少一半,造成空间浪费。

【解决】将新声代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存时只使用Eden和其中一块Survivor。HotSpot默认Eden和Survivor比例8:1,

当Survivor空间不足以容纳一次MinorGC之后存活的对象时,通过分配担保机制直接进入老年代。

【标记-整理算法】

标记复制算法在存活率高的情况下就需要进行较多的复制,效率会降低。

针对老年代存活率高的对象,使用标记-整理算法。

标记所有存活的对象,让所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

老年代使用标记清除和标记整理算法的对比:

使用标记整理的弊端:

老年代每次回收有大量存活的对象,移动存活对象并更新所有引用这些对象的地方必须全程暂停用户应用程序才能进行。

使用标记清除的弊端:

造成的空间碎片化问题只能依赖更为复杂的内存分配器和内存访问器来解决

加重了内存访问的复旦,因为内存访问时程序最频繁的操作,会直接影响程序的吞吐量。

总结:标记整理可以照顾吞吐量,标记清除可以照顾停顿。

如果关注吞吐量使用标记整理,关注停顿使用标记清除。