对垃圾回收算法的改进
[content_hide]
> 作者:李少博
> 链接:https://juejin.cn/post/6905300467027771405
下面介绍的两种垃圾回收算法,会对基础算法中内存碎片化、暂停时间过长、空间利用率不高等不足进行改进。
##### (1)分代算法
分代算法是基于这样一个假说:绝大多数对象都是朝生夕灭的。这个假说其实已经被证实了,这一点也不难以想象,绝大多数OLTP应用中一个对象创建出来很快就会被回收掉。分代算法把对象分类成几代,针对不同的代使用不同的 GC 算法:刚生成的对象称为新生代对象,对新对象执行的 GC 称为新生代 GC(minor GC),到达一定年龄的对象则称为老年代对象,面向老年代对象的 GC 称为老年代 GC(major GC),新生代对象转为为老年代对象的情况称为晋升。但是代数也不是越多越好,综合来看,代数是两代或者三代是最好的。
“`
这里引进了一个新的概念,OLTP和OLAP,这是两种不同的类型的应用,OLAP一般是数据分析型应用,OLTP应用是普通的实时类型
业务的应用。这两种应用的jvm优化是不一样的,这里你先记住这个概念,后边我们再具体讲优化。
复制代码
“`
在经过新生代 GC 而晋升的对象把老年代空间填满之前,老年代 GC 都不会被执行。因此,老年代 GC 的执行频率要比新生代 GC 低。通过使用分代垃圾回收,可以减少 GC 所花费的时间。
##### (2)增量算法
增量算法主要是通过并发的方式来控制STW(stop the world即应用停顿)时间。具体体现在垃圾回收线程工作和用户线程工作是以并发的方式来执行的。这点是因为jvm采用的是可达性分析算法,基于可达性分析的标记算法的工作过程一般都会分为数个阶段,并不是每个阶段都需要停止用户线程,所以这些不需要停止用户线程的阶段只需要和用户线程并发执行即可(增量如果不明白不着急,到后边讲垃圾回收器的时候会让你更好的理解)
### 四、使用改进的垃圾回收算法带来的问题及解决方式
上面的改进也是有代价的,因为这类算法的本质其实就是在时间和空间之间做权衡,如果速度变快了,那就得牺牲一定的内存,如果占的内存小了,那速度肯定就会相对慢一些。
##### (1)标记过程中对象引用关系发生变化
我们先来看看标记算法中的标记过程,这个过程可以用三色算法来抽象概括,即根据标记的不同程度将对象分成三类,白色:未被垃圾回收器标记的对象,灰色:自身已经被标记,但其拥有的成员变量还未被标记,黑色:自身已经被标记,且对象本身所有的成员变量也已经被标记。
在GC开始的时候所有对象都是白色如状态1,首先从GCRoot扫描的时候,将图中A标为灰色,E标为黑色如状态2,然后从灰色对象出发(不再扫描黑色),将灰色引用标记为黑色(A对象),再将B标记为灰色,如状态3,再递归循环从灰色出发扫描的过程,到了最后,只有D不会被扫描到,所以D就是垃圾,垃圾将在清理过程中被清理掉。
![img](https://y.gschaos.club/image/gc0929_1.jpg)
问题1:假如在状态3, jvm准备进行下一步标记的时候,A和B的引用关系被解除了,那么在下次标记的时候依然会从B出发把B标为黑色,把C标为灰色,最后B和C都会被标记上,但是其实我们根据图很容易推断出A和B解除了引用关系之后B和C从GCRoot是不可达的,因此B和C在这轮回收中是无法被回收的,于是B和C就变成了浮动垃圾。
问题2:另外一种情况发生在状态2结束,jvm准备进行下一步之前,E引用了B,而A和B的引用关系被解除,然后B和C也就无法在下一轮扫描中被扫描到了(因为下次扫描只会从灰色对象出发),接下来B和C就变成了垃圾,这就很严重了,因为影响到了程序的正确性。jvm为了解决这两个问题引入了读写屏障,读写屏障发生的条件如下
“`
问题2这个场景的代码如下:
void test(A a,E e) {
B b = a.b; // 触发读屏障
a.b = null;
e.b = b; // 触发写屏障
}
复制代码
“`
具体是:(1)写屏障:在e.b=b的时候,把e对象或者b对象标为灰色,这样下次扫描就可以扫描到了,(2)读屏障:在给B b赋值a.b的时候,立刻将a.b标记为了灰色,保证扫描的正确性,这种也叫做增量更新。
##### (2)跨代引用
使用分代也是有代价的,新生代中的对象不一定仅仅只是被GCRoot引用,还有可能被老年代对象引用,如图,想要知道B能否被回收,就必须扫描一下老年代,但这样就失去了分代的意义,jvm必须做到在安全的回收新生代的同时不扫描老年代,不然的话还不如不分代。
![img](https://y.gschaos.club/image/gc0929_2.jpg)
上图中,我们可以把C也作为一个GCRoot,从而每次扫描的时候可以从C出发,或者是将C标为灰色(写屏障)。将C作为GCRoot的这个方法具体是每当发生跨代引用的时候,就将老年代对象记录进一个名为Remember Set的集合中,然后扫描的时候也会以Remember Set中对象作为GCRoot,这样就避免了扫描整个老年代的问题。
[/content_hide]