垃圾回收算法大汇总(包含HotSpot中的算法实现)


前言(理论铺垫)

技术式垃圾收集和追踪式垃圾收集分别对应直接收集和间接收集。

分代收集理论

把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。
在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

跨代引用

建立在两个假说之上
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
所以根据这两个假说得出结论:跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极
少数。
当存在跨代引用时,这个关系就不会消亡,直到其中一个对象也进入老年代,这样子就不存在跨代引用了,所以我们不需要扫描整个老年代去搜寻跨代引用。
而是采用记忆集的方式,记忆老年代的哪几块内存存在跨代引用,那每次扫描扫描这些就行了。


垃圾回收算法

1.标记清理算法

通过标记某个对象(可以是只标记存活对象,也可以是只标记需要被清理的对象),该方法虽然简单,但效率差需要一个一个清理(新生代存活的从来只占少数,需要清理的很多),因为每个节点你都要去扫描并进行标记,且会产生大量的内存碎片。

2.标记复制算法

还是需要先去标记每一个需要被清理的对象,但是将内存分为两块,每一段时间仅使用其中的一块,垃圾清理时,将存活的对象全部复制到另一个内存块(按照顺序复制到另一块内存,所以该内存存储的数据是连续的,不产生内存碎片),一次性清除之前的内存块。

3. Appel式回收

将内存分为Eden和两个Survivor区,当进行垃圾收集的时候,把Eden和其中一个Survivor区的存活对象复制到另一个Survivor区,并清除掉之前的Eden区和Survivor区。
Eden:Survivor大小大概是8:1,请始终牢记存活的一般情况下都只占少数,所以一个Survivor区很容易容纳两个区的存活对象。当然也不能保证,那么我们就需要老年代的内存进行分配担保,保证Survivor区放不下时,还能够放到老年代,因为已经经历过收集了,说明它有资格进入老年代。

4.标记整理算法

标记过程和标记清理算法一样,不同的是标记完成后,就将所有存活对象移动,顶替掉不少需要被清理对象的内存,使它们更紧凑。
该方法的缺陷是:老年代中存活数量占比更大,移动操作负重更大,需要采用STW操作才能够进行
但是该方法的吞吐量是占优势的,因为内存分配和访问比起清理更加频繁,如果不采用移动的方式来整理内存,减少内存碎片,内存分配和访问将会负重更多且频繁负重。

5.和稀泥算法

当内存碎片较少,没有内存分配压力时,采用标记清除算法,当内存碎片较多,内存分配压力过大,采用标记整理法

HotSpot算法实现

1. 根节点枚举

指寻找与根节点直接相连的各个节点,该枚举必须要STW

2.OopMap

HotSpot使用OopMap来提升追踪查找与Root相连接的节点的效率。因为一个类加载进内存时,它的空间是确定的,即哪些字段需要占用多少内存,当然某些字段是否被引用也被确定了,而且加载进内存的数据一定是连续存放的,这时,我们采用指针偏移的方法准确地指明哪些字段有引用关系,并把这个记录放在OopMap中,这样就不用扫描整个对象找引用了。
每个线程创建的栈帧会标注在哪个地方存在引用,这个数据也会存入OopMap

3. 安全点

3.1安全点选取标准

选取安全点的特征:是否能让程序长时间运行
原因:为了保证需要STW时,每个线程都能尽快到达安全点,我们可以采用在每个指令后面都设置安全点,这样子不管哪个线程执行到哪一步了,只要安全点量够大,执行完每一条指令都可以在安全点停下,但是安全点是会消耗内存的,这样的做法严重地增加了虚拟机的压力,但是我们又没有能力把一个指令写得很长很长(虚拟机不会因为指令长就延长该指令的运行时间),让大部分程序都跑这个长指令,指令结束就能到达安全点。
所以有一个办法就是在一个会复用的指令后面加上安全点,指令一直复用就相当于程序可以长时间的运行某个指令,且每次到达指令结尾都可以选择进入安全点,这样子可以在长时间执行的情况下等待其他线程进入安全点,也保证了线程都能尽快找到安全点。
方法调用,循环跳转以及异常跳转都能满足上述的条件。

如何去提醒所有线程需要中断,赶紧找安全点
1)抢先式中断

当需要STW时,先终止所有线程,未到达安全点的恢复该线程让其运行到安全点,几乎没有虚拟机采用这种方法。

2)主动式中断

生成一个标志,每个线程在一定时间里来轮询这个标志,如果该标记为真,轮询的标志位置与安全点重合,所以当线程轮询到该标志为真时,可以较快地找到安全点。因为轮询操作方式注定它会频繁执行,所以为了保证效率不让轮询操作占用大量的时间,需要将轮询指令精简。

3.2 安全区

当有些程序处于不执行的状态,比如Sleep和Blocked状态,这种状态根本就不能响应虚拟机的中断请求,这个时候我们就需要安全区,当程序进入安全区后,会通知虚拟机或者推出一个标志代表自己进入了安全区,需要出安全区时也得先确认虚拟机是否已经完成了根节点枚举或者查看是否现在正在处于垃圾收集阶段,如果都完成了,才可以出安全区,不然需要一直等待。

4. 记忆集

记忆集是虚拟机中非垃圾收集区域指向垃圾收集区域的指针集合的抽象数据结构,目的是为了防止存在老年代对新生代的跨代引用,而去扫描整个老年代。

4.1三种精度

1)字长精度

每个记录精确到系统能访问的最大字长所包含的内存,即在每个32位或64位地址寻址范围中,存在跨代指针。

2)对象精度

对象中含有跨代指针。

3)卡精度

某块内存区域含有跨代指针。(也被称为卡表,通常使用这种精度)。该方法实现的最简单方式,是使用一个字节数组byte[],每一个字节元素都代表一段2的n次幂的内存区域,即一个卡页。当卡页有字段包含跨代指针,则表示该卡页的字节元素变脏,该字节元素就变为1

4.2 写屏障

写屏障可以看作是虚拟机确立引用关系这个过程中的AOP切面,即用该屏障包围引用关系确认执行程序,只要确认或建立引用关系,切面中的代码就执行去维护和修改记忆集

伪共享问题

缓存中存储数据的方式是以一行一行为单位的,读写也是以一个单位读写,这代表一个单位中有很多不同的连续数据,但是当中央处理器(CPU)读写或修改其中一个数据时,需要更新某个数据时,会把整个一行存储单位的其他数据认为是无效或者是过期数据,那么其他数据重新存入缓存或被CPU操作时,就需要更多的开销(在多核计算机中,每个CPU(核)都有自己的缓存空间,如果几个缓存空间都对应着内存中一行数据,这一行中某些数据发生修改时,则所有核都要重新存储这一行的数据)
简单解决方法:检查卡表元素是否被标记过,没被标记过再去修改,这样大大减少了去修改卡表的次数。

5.并发(用户线程和垃圾标记并发)

5.1问题出现原因

当并发的时候,可能会出现垃圾把一个对象标记为存活,但是下一秒用户线程就取消了对其引用,该对象应该变为垃圾对象的,但是对该对象的标记已经结束,产生了浮动垃圾,这个可以容忍,下一次垃圾收集处理掉就行了。但是把标记为垃圾的对象重新建立引用,这就很危险了,因为该对象本来应该存货却被收集了,称为对象消失
我们用颜色来表示该过程,白色表示还没被扫描或标记,黑色代表该对象上所有引用都被扫描了,灰色表示该对象还有引用没被扫描到,即该对象处于被扫描状态。

5.2 解决方式(增量更新和原始快照)

研究表明:出现对象消失这种问题需要同时满足下列两个条件:
1、 赋值器插入一条或多条从黑色到白色的引用
2、 赋值器删除所有灰色到白色的引用
因为扫描是顺序链式扫描,不会倒回去扫描,引用关系可以看作这个“链”,当灰色对象a到白色对象b的链消失了,黑色对象c却和b建立起了新的链,这个时候扫描继续顺着链子扫描下去,根本回不到c到b的链上(因为c的相关链虚拟机默认已经扫描完了,所以才标记为黑色),扫描系统扫描不到b,认为其与Root没有连接,就会视其为垃圾,但其实b不是。
两个条件需要同时满足才会出现对象消失,我们只需要破坏其中一个条件

1、 增量更新(破坏第一个条件),当有新的黑到白的引用建立,记录下来,第一次扫描完成后,把这几个黑的再扫描一遍。
2、 原始快照,当灰到白的所有引用关系被删除,则记录下这个灰色对象,第一次扫描完成后,再将这些灰色作为根节点扫描一遍,即保存下第一次扫描灰色节点之前的图,第一次扫描之后再按照这个图扫描一遍,因为GCROOT一旦确定,图就确定了。之前我总以为需要把第一次扫描的整个图再重新扫描一遍,不是很浪费时间?忘了之前我自己写的,扫描不能反着链子扫描回去,所以也只是扫描灰色对象之后链子连接的对象

两种方式优缺点对比

在增量更新的过程中,因为要随着黑和白之间的引用更新同时记录,这就涉及到引用更新和增量更新两个操作的并发,消耗的资源比较多。
原始快照存储的就是一个时间节点之前的状态,这状态已经发生了,确定了则不需要随着引用更新同时去并发更新
记录的插入和删除,都是用写屏障实现的。
CMS用增量更新,G1用原始快照

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值