彻底理解JVM垃圾回收-垃圾收集算法(八)

分代收集理论

当前商业虚拟机的垃圾收集器大多数遵循了”分代收集“的理论进行设计的。遵循分代收集理论,Java的堆空间被划分为新生代(Yong Generation)和老年代(Old Generation)两个区域。在新生代中,每次垃圾收集时都能发现有大批的对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

标记-清除算法

该算法分为两个阶段,”标记阶段“和”清除字段“:首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记过的对象,也可以反过来标记所有存活的对象,清理未标记的对象。标记过程就是对象是否属于垃圾对象的判定过程。该算法主要存在两个缺点:
(1)执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要回收的对象,这时必须进行大量标记和清楚的动作,导致标记和清除两个过程的执行效率都随着对象数量增长而降低
(2)内存空间的碎片化问题,标记、清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得提前触发另一次垃圾收集动作。
标记清除算法的执行过程如图:

标记清除算法

标记-复制算法

标记-复制算法简称复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。它可将内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一框的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象还是存活的,这种算法将会产生大量的内存空间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这种实现简单,运行高效,但是缺陷也是显而易见的,这种复制回收算法的代价是将可用内存缩小为原来的一半,造成空间浪费的。复制算法的执行过程如图:
复制算法
现在商用的Java虚拟机大多数优先采用了这种收集算法去回收新生代。因为新生代对象大多数情况下都是”朝生夕灭“(第一次Yang GC基本大多数对象都可被回收),所以新生代内存并不需要将内存按照1:1的比例进行划分新生代的内存空间,而是将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象复制到另一个Survivor区域,然后直接清理Eden和已经使用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1:1,即每次新生代中可用内存空间为整个新生代容量的90%(Eden空间+一个Survivor区域),只有一个Survivor(10%)的新生代空间会被“浪费”。当然,对象经历回收后,如果仍然有超过10%的内存空间的对象存活时,就需要进行内存分配的担保措施,一般就需要依赖其他内存区域(大多是老年代)进行担保分配。如果另一块的Survivor区域没有足够的空间存放存活对象,那么这些对象便通过分配担保机制直接进入老年代。

标记-整理算法

标记-复制算法在对象存活率比较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
针对老年代的对象的存亡特征, 产生了标记-整理算法,其标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接清理可回收对象,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的执行过程如图:
标记整理
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:
如果移动存活对象,尤其在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重​的操作,而且这种对象移动操作必须全过程暂停用户应用程序才能进行(Stop the Wold->STW)。但是如果跟标记-清楚那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象将导致空间碎片化问题只能依赖更为复杂的内存分配器和内存访问器来解决。

在这里插入图片描述

发布了41 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Yunwei_Zheng/article/details/105293395