本章主要是对上一篇文章讲的垃圾回收机制的扩展,垃圾回收其实本身是有很多可以优化的点的,本章就进行对这些优化点进行介绍。
1.GC Roots遍历提升效率
以往做法
当垃圾回收器线程进行GC时, 第一步需要 「找到GC Roots」 ;第二步通过GC Roots进行 「遍历堆中引用GC Roots的对象形成引用链」 ;第三步,将不在引用链中的对象标记进行 「标记」 (需要回收的对象),或者 「标记」 引用链中的对象(需要复制,整理的对象),具体标记哪种对象根据堆中的分代内存不同和采用的垃圾回收算法来确定。
可优化地方以及优化原理
上述过程第二步中遍历堆中引用GC Roots的对象,这部分随着堆内存的越来越大需要的时间也会逐步增长。如果能够提前知道堆中哪部分内存是引用,来判断是否引用GC Roots这样效率是不是会更高一些。
没错,因为之前讲过从Exact VM开始就已经采用了准确式内存管理即知道哪部分内存是引用;而且在即时编译的过程中我也会知道栈中或者寄存器里哪部分内存是引用。这个时候我用一个数据结构来存储这些信息,在第二步中就不需要遍历整个堆了,只需要遍历没有标识引用内存的地方(也就是刚才数据结构中没有存储的信息)。
在HotSpot中使用OopMap这个数据结构来存储这信息,也就是可以显著提高GC Roots遍历的效率,但是在什么位置放这些信息呢?
2.提升了GC Roots遍历效率却不知道怎么安插?
前面提到过通过一个OopMap数据结构能够提升遍历效率,但是OopMap中的数据在不同的地方内容是不一样的(比如每个方法里面我的局部变量表里面的内容可能是不一样的),所以 我为每个指令附近都放一个OopMap 。
❝
等等,这样未免也太浪费内存了吧~。
❞
没错,所以我们得先办法把它放到合适的地方!嗯没错,我想想: **「这个数据结构的出现是为了优化GC第二步的效率出现的,也就是说只有GC时在放这些数据就行了~。思路找到了,但是什么时候发生GC呢?
发生GC这个时间我不能确定,但是我可以确定的是它遍历堆中内存的时候必须要进行STW【否则如果在标记的过程中堆中引用发生变化就会导致标记结果出错】(2.1中讲解),我指定只有代码中执行执行到某个地方才可以进行STW这样我就可以间接的实现我的目的」** 。
也就是说当GC发生时,只有执行到某个地方才会进行STW,然后我在这个地方附近放上这么一个OopMap的数据结构,然后加快第二步的效率。
「这个某个地方其实名字叫做“safePoint”」 ,顾名思义安全点,只有代码执行到安全点附近才可以进行STW垃圾收集,而只要将OopMap安插到安全点附近就行。
2.1为什么需要STW?
上面提到过:
❝
【否则如果在标记的过程中堆中引用发生变化就会导致标记结果出错】
❞
一,三色标记法
接下来用三色标记法进行解释如果没有STW会发生什么情况:一,先解释三色标记法:
二,没有STW出现的情况
在这里插入图片描述
三,解决方案
上面那种异常情况必须同时满足两个条件:1.灰色对象不引用白色对象 2.黑色读写引用白色对象
因此,只要让其中一个条件不满足即可,因此出现了两种解决方案:1.增量更新:这种方案是让第二个条件不满足,即当黑色对象引用白色对象时,将这个黑色对象保存下来,等扫描结束后,再次取出黑色对象进行扫描,可以简单理解为如果黑色对象引用了百世对象就会被标记为灰色。
2.原始快照:当灰色对象删除白色对象的引用时,将这个灰色对象记录下来,等到扫描结束后,在对这些灰色对象为根进行扫描,简单理解为:不管是否删除与否都会按照第一次刚开始的引用关系图进行扫描。
❝
CMS垃圾回收器采用增量更新来进行并发标记,G1,Shenandoah采用原始快照
❞
3.safePoint我又该放到哪里?
safePoint上面解释过了,但是我该在哪里放置safePoint呢?放的多了会导致GC收集过于频繁增加运行时内存压力,放的少了又会因为堆中不断增加使用的内存而没有及时回收堆里面内存导致垃圾收集器等待时间过长。
这样,我定义一个规则,只有这种**“会让程序长时间运行的指令”**特征我才会进行安插safePoint,但是这个特征“长时间”并没有具体的定义,但是却有“指令序列复用”这样的含义。比如方法调用,循环调整,异常跳转这些,只有这些指令附近才会安插safePoint。
safePoint位置选好了,但是上个问题说过执行到safePoint中需要进行STW,发生GC时,我该如何快速跑到safePoint附近进行STW?还有我这个STW该怎么实现呢?
4.如何实现STW?
首先解释为什么叫做STW,全称“Stop the Word”,因为 「通过GC Roots遍历堆中内存的过程其内存里面的引用关系不能发生变化」 ,所以需要暂停所有的用户线程操作来保障Gc Roots形成的引用链是正确的即待会标记过程不会出错。
让所有线程都暂停,这个“看起来复杂其实并不简单”的操作其实有两种方式处理:一,抢先式中断:
❝
垃圾收集器收集时,系统将所有用户线程都中断。当发现不在safePoint附近的线程时先让他恢复运行直至跑到safePoint附近。这种方式现在几乎没有虚拟机采用这种方式来响应GC。
❞
二,主动式中断:
❝
我不直接对我的用户线程操作,当发生GC时,我给用户线程设立个标志位,用户线程执行的时候不断轮询这个标志位,如果轮询到了那么我将自己中断我自己的运行,由于这种方式是轮询到就立马进行挂起所以将轮询的地方和safePoint的地方重合。
❞
优化
“不断轮询标志位”这句话听起来就很耗时哈哈,那么再虚拟机中是怎么优化的呢?还有轮询之后的操作我自己挂起我自己这个又是怎么实现的?
等等,我不放到下一个问题里面讲了,直接一遍过:
轮询标志位这个操作其实就是一条汇编指令,(对于汇编和JAVA是什么关系,之前也有讲到过,辛苦翻阅前面文章~) 这条汇编指令的意思就是当我轮询到需要中断线程的标志位的时候:我会将其中一个内存页设置为不可读,这会导致产生一个自陷异常信号,异常处理器中接受到后进行主动中断操作。
-
编程
+关注
关注
88文章
3615浏览量
93713 -
GC
+关注
关注
0文章
9浏览量
17083 -
JVM
+关注
关注
0文章
158浏览量
12223
发布评论请先 登录
相关推荐
评论