jvm之垃圾收集算法

垃圾收集算法的可以归为两大类,引用计数式垃圾收集、追踪式垃圾收集。由于主流的java虚拟机没有采用引用计数式收集算法,所有我们主要讨论的是追踪式垃圾收集算法。

追踪式垃圾收集算法可以细分为四类:

  • 分代收集。
  • 标记-清除算法。
  • 标记-复制算法。
  • 标记-整理算法。

分代收集理论

当前大多数虚拟机的垃圾收集器都遵循分代收集理论,主要因为以下两方面:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过多次垃圾收集都还存活的对象就越那以消亡。
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

通俗的讲就是将堆空间分成不同的区域,将新的对象都放到指定的一个区域,该区域频繁发生垃圾回收,将存活下来的对象放到另一个区域,该区域收集的频率很低,这样可以提高垃圾回收的效率,这样每次回收只需要回收某一区域而不是对整个堆空间的回收。
堆空间被划分为新生代和两年代,新生代被回收后存活的少量对象会逐步晋升到老年代空间。

在分代收集算法中对堆空间的回收分为minor gc,major gc,full gc

  • minor gc回收新生代。
  • major gc回收老年队。
  • full gc回收整个堆。

对于跨代引用问题由于极少数对象存在跨代引用关系,所以在发生minor gc时,不用把整个老年代对象加入gc roots中进行回收,而是用一个特殊的数据结构来维护(记忆集),该数据结构会把老年代分为不同的小块,包含跨代引用块中的对象才会被加入到gc roots中。

标记-清除算法

该算法分为两个阶段:标记和清除,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收未被标记的对象。标记清除算法的主要问题就是内存碎片化现象严重,这会导致如果有新的对象需要分配内存空间时,此时碎片空间加起来刚好够分配,但是不是连续的,此时就会提前触发垃圾收集。
在这里插入图片描述

标记-复制算法

将内存按容量分为大小相等的两块,在发生垃圾收集时,假设分为A和B两块区域,A是已经分配的空间,B是空闲的空间,此时将A中存活的对象统一复制到B中,然后清除掉A的空间,这样存活的对象复制到B空间后都是连续规整的,在分配对象时只需要按顺序分配就可以了,很好的解决了内存碎片化的问题,但是它的缺点想必大家都可以看到,那就是有一半的空间在浪费着。
在这里插入图片描述
那么根据分代收集理论,该算法比较适合在新生代中使用,因为新生代当中的对象基本都是可以被回收掉的,那么用这种算法只需要复制少量的存活对象。基于该算法会把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存时只使用Eden和一块Survivor空间。在发生垃圾收集时,将Eden和Survivor空间中存活的对象一次性复制到另外一块Survivor空间中去,然后直接清理掉Eden和Survivor空间。在Hotspot虚拟机中,Eden和两个Survivo区的比例默认为8:1:1。对于回收后的对象在Survivor中放不下时,会放入老年代空间当中。

标记-整理算法

标记-复制算法在面对大量存活对象时执行效率就会变低,毕竟要复制大量的存活对象。针对老年代大部分对象都是存活的特征,标记-复制算法可以来解决该区域的回收,该算法的标记过程仍然和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
在这里插入图片描述
移动存活对象,像老年代这种存在大量的存活对象,在对象移动过程中必定要暂停用户线程才可以,也就是说需要Stop the world。但是如果像标记-清楚算法那样不考虑移动和整理对象的话又会导致严重的内存碎片问题,为了解决这种碎片化的内存分配问题只能依赖更为复杂的内存分配器和内存访问器来解决,内存的访问是非常频繁的在这个环节上如果增加负担的话,必定会降低系统的吞吐量。移动那么回收时会比较复杂,不移动分配时会比较复杂,因此基于这两点诞生了不同的垃圾收集器,比如对系统吞吐量要求高的Parallel Scavenge收集器采用的就是整理算法,对响应时间要求高的CMS收集器就采用的是标记清楚算法,cms收集器在多数时候采用标记-清楚算法,暂时容忍内存碎片的存在,直到内存碎片已经严重影响对象的分配时,再采用标记-整理算法收集一次。

根节点枚举

垃圾收集器在工作时需要借住所有的根节点(Gc roots)来判断对象是否在引用链上,在确定根节点的过程中需要暂停所有的用户线程(Stop the world),保证在一个快照下进行根节点的枚举,不管哪一款垃圾收集器在进行根节点枚举的时候都会触发Stop the world。

安全点

安全点就是在发生Stop the world时,用户线程必须运行到安全点的位置才会停顿下来,方法调用、循环跳转、异常跳转这些地方是适合设置为安全点的地方。具体怎么中断用户线程呢,主要分为抢占式中断和主动式中断。

  • 抢占式中断:当需要中断时,虚拟机强制中断所有线程,然后依次判断每个线程是否到达安全点,如果没有到达就让用户线程继续执行到安全点位置,不过现在没有虚拟机采用这种方式了。
  • 主动式中断:虚拟机会设置一个中断标志位,所以用户线程到达安全点的时候主动去轮询该标志位看是否需要暂停。

安全区域

安全点看似已经完美解决了用户现场停顿的问题,但是有一种情况是什么呢。用户线程如果还没有到达安全点的时候处于阻塞或者Sleep状态呢,这样用户线程就没法及时的响应虚拟机的停顿要求,虚拟机也不可能一直等着用户程序继续执行到安全点的位置,此时就需要用到安全区域,所谓安全区域就是用户线程在执行到一定区域内的时候,在该区域内对象的引用肯定不会发生变化。用户线程在进入安全区域是会标记自己进入了,如果此时垃圾收集工作开始,虚拟机就不用管这些用户线程,在用户线程即将离开安全区域的时候,用户线程会看下现在虚拟机是否在进行根节点枚举,如果是的话,用户线程就会一直停顿直到收到可以离开的信号为止。

记忆集与卡表

记忆集就是在新生代中维护一个数据结构,为了区分老年代中那些对象存在跨代引用关系,在进行垃圾回收时将这些存在跨代引用的对象也加入Gc roots当中。卡表是记忆集的具体实现,卡表的数据结构如下所示:
在这里插入图片描述
卡表就类似数组,每个元素存的是指向一个卡页的指针,每个卡页的大小为512字节。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个对象存在跨代引用,那就将对应卡表数组元素的值标记为1,称为这个元素变脏,在进行垃圾收集时,只要筛选出标记为脏的元素所对应的卡页,然后加入Gc roots中。

写屏障

写屏障主要针对的是卡表的更新,就是对象的引用放生变化时需要及时更新卡表,虚拟机会在对象引用字段赋值后面加入写屏障指令,用来对卡表进行更新。

努力!奋斗!进大厂!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值