Java 垃圾收集之垃圾收集算法及 HotSpot 算法实现

垃圾收集算法

上一篇博我们探讨了垃圾收集时如何判断对象是否存活,这篇探讨一下垃圾收集器是怎么回收内存的,首先介绍三种垃圾收集算法。

标记 - 清除算法

最基础的收集算法是“标记 - 清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记后统一回收所有被标记的对象。这种方式有两个不足:一个是效率低,另一个就是空间问题,清除之后会产生大量不连续的内存碎片,这会导致后面分配大对象时由于无法找到足够大的连续内存而不得不触发一次垃圾收集动作。标记 - 清除算法执行过程如图:
在这里插入图片描述

复制算法

为解决效率问题,复制算法出现了,它可以将内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,复制算法执行过程如图:
在这里插入图片描述
由于新生代中的对象绝大部分是“朝生夕死”的,所以并不需要按照 1 : 1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间,最后清理掉 Eden 和刚才使用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8 : 1,也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有10%的内存会被“浪费”。当 Survivor 空间不够用时,需要依赖其它内存(老年代内存)进行分配担保(通过分配担保机制进入老年代)。

标记 - 整理算法

复制收集算法如果在对象存活率较高时就需要进行较多的复制操作,效率将会很低,而且还需要额外空间进行分配担保以应对被使用的内存所有对象都100%存活的极端情况,所以老年代一般不使用这种算法。
老年代使用标记 - 整理算法清理对象,该算法标记过程与“标记 - 清除”算法一样,但是后续是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,如图:
在这里插入图片描述

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法根据对象存活周期不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

HotSpot 的算法实现

枚举根节点

从可达性分析中从 GC Roots 节点找引用链的操作为例,可作为 GC Roots 的节点主要在全局性的引用(例如常量/静态属性)与执行上下文(例如栈帧中的本地变量表)中,要检查这些引用,那么必然消耗很多时间。
另外,可达性分析必须在一个能确保一致性的快照中进行,“一致性”指的是在整个分析期间整个系统看起来就像是被冻结在某个时间点上,不能出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法保证。这点是导致 GC 进行时必须停顿所有 Java 执行线程(Sun 将这件事称为“Stop-The-World”)的其中一个重要原因。
目前的主流Java虚拟机使用的都是准确式 GC(虚拟机知道内存中某个位置的数据具体是什么类型),所以当执行系统停顿下来之后,虚拟机应当知道哪些地方存着对象的引用。在 HotSpot 的实现中,是使用一组称为 OopMap 的数据结构来达到这个目的的,在类加载完成的时候,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来。这样 GC 扫描时就可以直接得知这些信息了。

安全点

在 OopMap 的协助下,HotSpot 可以快速且准确的完成 GC Roots 枚举,但一个现实的问题随之而来:可能导致引用关系变化,或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将需要大量的额外空间,这样 GC 的空间成本将会变的很高。
实际上,HotSpot 没有为每条指令都生成一个 OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始 GC,只有到达安全点时才能暂停。Safepoint 的选定既不能太少以至于让 GC 等待时间过长,也不能过于频繁以致于过分增大运行时负荷。所以 Safepoint 的选定基本是以“是否让程序长时间执行的特征”为标准进行选定的,“长时间执行”最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 Safepoint。
对于 Safepoint,还需要考虑如何在 GC 发生时让所有线程都跑到最近的安全点上再停顿下来。这里有两种方案可供选择:

  1. 抢先式中断:不需要线程的执行代码主动配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机采用这种方式响应 GC 事件。
  2. 主动式中断:GC 需要线程中断时,不直接对线程操作,仅简单设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就主动自己中断挂起。轮询标志的地方是和安全点重合的。

安全区域

Safepoint 看起来解决了如何进入 GC 的问题,但是在程序“不执行”(没有分配 CPU 时间,如处于 Sleep 状态或者 Blocked 状态)的时候,这时线程无法响应 JVM 的中断请求,这时就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的,可以把 Safe Region 看作是被扩展了的 Safepoint。
线程执行到 Safe Region 中的代码时,首先标识自己进入到了 Safe Region,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为 Safe Region 状态的线程了。在线程要离开 Safe Region 时,它要检查系统是否已完成根节点枚举(或是整个 GC 过程),如果完成了,线程继续执行,否则必须等待直到收到可以离开 Safe Region 的信号为止。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值