JVM垃圾回收之垃圾收集算法

对于jVM垃圾的回收,说白了也就是两步,一步就是标记,另一步就是收集。上一节我们讨论了如何去标记一个对象是否应该删除,那么这次我们讨论一下对于标记后的对象和内存怎么处理的问题。当然,这两个问题的思路搞明白了,我们就可以好好看一下JVM提供的垃圾回收器了。目前,主要的垃圾收集算法包括“标记-清除算法”、“复制算法”、“标记整理算法”、“分代收集算法”等。接下来我们就逐一解开他们神秘的面纱。

1.标记-清除算法

1.1 概述

“标记-清除”见名知意,就是分为两步完成,分别是标记和清除两个阶段。对于标记阶段,其过程在上一节已经讲述判定的过程;对于清除阶段,直接将标记的对象统一完成收回。由此也成为最基础的算法,之所以成为基础算法,是因为他对于标记的对象简单粗暴的清理。

1.2 缺点

通过上图很容易看出来标记清除之后会产生大量的不连续内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。另外,效率也是一个很严重的问题。

2 复制算法

2.1 概述

为解决效率问题,一种称为“复制”的收集算法出现了。它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。还记得上上节提到的内存模型中的FromSurvivor和ToSurvivor吗?他就是复制算法最好的印证。这两块区域来回切换FromSurvivor和ToSurvivor区域。不过如下图,内存确实是耗费不少。

2.2 特点

1)实现简单,运行高效,只要移动堆顶指针,按顺序分配内存即可。

2)这种算法的代价就死将内幕才能缩小为原来的一半,未免太高了。

2.3 使用案例及优化

研究表明,朝生夕死的对象一般占到98%。所以对于这两个区域不需要按照1:1的比例还划分。而将内存较大一块区域(Eden)空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间将存活对象放置在另外一块Survivor空间。在一定程度上节约了内存空间。这样,100%的空间就能做180%的事,空间利用率节省了80%左右。按照HotSpot虚拟机的算法,Eden区域和Survivor区域分配比例为8:1。也就是说90%的对象经过标记后,会把剩下的对象放到10%的空间。当然,大多数情况下,这是没问题的。当Survivor空间不够用时,需要依赖其他内存(老年代)作为分配担保。

3 标记整理算法

对于标记整理算法,标记过程仍然与“标记-清除”算法一致,只不过在清除过程发生了变化。他是让所有存货的对象都向一端移动,然后直接清理调边界以外的内存,在这个过程中,存活对象会覆盖被判死刑的对象。由此可见,标记整理算法更适合老年代。

4 分代收集算法

这种算法其实并不能称得上真正的算法,只是根据对象存活的周期的不同将内存划分为几块。这其实挺类似于孔子提倡的因材施教,因地制宜的想法。比如,新生代的对象,它的最大特点就是朝生夕死,一天下来根本存活不了多少对象,再加上他有一个实力强悍的老爸(内存占到堆内存的2/3)为其撑腰,比较适合使用复制算法。而这位强悍的爸爸一旦没有空间,他可没有强有力的爸爸为他撑腰,所以就必须使用“标记-清除”或者“标记-整理”算法来进行。

5 HotSpot算法实现

5.1 枚举根节点

从可达性分析中得知,从GCRoots节点找引用链这个操作,GCRoots需要在全局的引用与执行上下文(栈帧中的本地变量)中。如果全部遍历,那么必然会消耗许多时间。而在这个过程中,需要停顿所有的线程(Sun将这个事件成为Stop the world),这样可以避免对应引用关系的不断变化。所以,JVM就想到一个办法,在类加载或者对象创建的时候,会标记存放对象的引用位置(HotSpot称之为OopMap的数据结构).当GC再次发生的时候,虚拟机会直接定位到所标记的位置上。这样,可以大大减少GC的时间。

5.2 安全点

5.2.1 概述

虚拟机在借组OopMap,可以快速准确的完成GCRoots的枚举,然而如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,GC的空间成本也会大大提高。为此,HotSpot设立了称之为安全点的东东,用以选择性的记录位置。即程序执行时并非在所有的地方都能停顿下来开始GC,而是只有在到达安全点时才能暂停。

5.2.2 设立准则

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”,长时间执行的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转,所以具有这些功能的指令才会产生SafePoint。

5.2.3程序的中断方式

当程序没有到达安全点,该如何执行呢?这里提供了两种方案供选择。一种是抢先试中断,另外一种是主动式中断。抢先试中断是当GC发生时,会把所有的线程全部中断,如果发现中断的线程不再安全点上,就让他跑到安全点,不过现在基本不适用这种方式。主动式中断是GC在需要中断线程的时候,不直接操纵线程,而是做一个标记。各个线程执行时主动轮询这个标记,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点时重合的,另外再加上创建对象需要分配的内存的地方。

5.3 安全区域

5.3.1 背景

安全点虽然解决了如何进入GC的问题,但是如果程序处于挂起状态呢,也就是说没有分配到CPU的时间(我没有分配到CPU,从何谈起指令,从而从何谈起安全点),典型的例子就是线程处于Sleep或者Block的状态。针对于这种情况,安全区域就派上用场了。

5.3.2 特点

安全区域是指在一段指令中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。

5.3.3 执行过程

当线程进入安全区域时候,首先标记自己已经进入安全区域。当JVM发起GC的时候,就不用管标识自己为安全区域的线程了。在线程要离开安全区域,它要检查系统是否已经完成了根节点的枚举,如果完成了,那线程就继续执行,否则他要等待直到收到可以安全离开安全区域为止。因为如果直接让线程执行而此时GC没有完成,那么这个线程很有可能改变引用关系,扰乱社会秩序,所以线程必须等GC完成后才能继续执行。

发布了72 篇原创文章 · 获赞 24 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/102731556