【JAVA虚拟机】垃圾收集算法详解

JVM虚拟机在判断哪些对象需要回收之后,接下来就是如何回收这些对象的内存。可以把系统内存想象成一个个小方格,jvm使用到的内存和需要收集到内存都散乱分布。如何将需要回收的内存回收,并尽可能提高效率和剩余内存的规整?

标记-清除算法

这个是最基础的算法,顾名思义,这个算法分为两步:

  1. 首先标记出所有需要回收的对象
  2. 在标记完成后统一回收所有被标记的对象。
    标记清除算法示意图

标记-清除算法的缺点:

  • 效率问题,标记和清除两个过程的效率都不高
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了
实现步骤:

  1. 将可用内存按容量划分为大小相等的两块
  2. 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
  3. 然后再把已使用过的内存空间一次清理掉
    复制算法示意图

缺点:将可用内存缩小了一半

在实际应用中,大部分新生代都使用这个算法,实际并不需要缩小一半这么多。IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。

标记-整理算法

与新生代不同,老年代的垃圾回收,一般大部分对象都会存活,如果使用标记-复制操作,不仅效率会降低,并且也必须按照1:1的比例来分配内存空间,造成50%的空间浪费。根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
在这里插入图片描述

分代收集算法

这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
  • 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收

HotSpot虚拟机中的算法实现

枚举根节点

垃圾收集的第一步,是确定哪些对象已死,这里使用的是可达性分析算法。第一步:枚举根节点,即确定可达性分析的GC ROOTS节点。如何根节点可能出现的地方很大,全部都需要扫描一遍,因此可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行,——这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。

这点是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为“Stop TheWorld”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

HoptSpot虚拟机中采用的是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置。HoptSpot虚拟机中使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,在系统停顿时,就不需要在一一遍历所有对象,只需要从OopMap直接获取根节点就可以了。

安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
所以,为了解决这个问题,引入安全点的概念,即:不是所有操作都记录OopMap,只在一些关键点记录。这就意味着,当jvm想要停止运行时,不是立刻停止运行,而是等所有线程全部跑到安全点才会真的停止。
这就引入了下一个问题:如何让所有线程都跑到安全点?现在主要有两种方案:抢先式中断(Preemptive Suspension)和主动式中断(VoluntarySuspension)

  • 抢先式中断 :需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。(几乎没有虚拟机用这个方法)
  • 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起

安全区域

安全点似乎完美解决了进程如何进入GC的问题,但是有个前提,所有的线程都要“跑”到安全点才可以,那么如果线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。

简单来说,就是一段代码呗标识为安全区域,线程执行到这里时,会标记线程处于安全区域。那么程序GC时,就不管这个线程,同时这个线程离开安全区域时,如果系统正在GC,也要等待GC完成之后再离开。
简要地介绍了HotSpot虚拟机如何去发起内存回收的问题,但是虚拟机如何具体地进行内存回收动作仍然未涉及,因为内存回收如何进行是由虚拟机所采用的GC收集器决定的,而通常虚拟机中往往不止有一种GC收集器,这个问题再下一篇博客中探讨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值