常见 GC 算法思想

2016-03-16 22:18:31   最后更新: 2016-03-17 13:33:05   访问数量:452




上一篇日志中,我们介绍了 java 虚拟机是如何判断是否应该对对象进行垃圾回收的:

java 对象存活判定算法

这篇日志中我们就来看一下具体的算法实现

 

 

 

如图所示,标记-清除算法是垃圾收集算法中最基础的一个,顾名思义,这个算法分为标记和清除两个阶段,在标记阶段,通过对内存区域的遍历,对需要回收的对象进行标记,然后在清除阶段将已经被标记的对象进行清除

在上一篇日志中,我们说过,事实上这个标记过程需要进行两次标记,从而选出没有 finalize 方法或者已经被执行了 finalize 方法的对象

这个算法有两个不足:

  1. 标记和清除两个过程的效率都不够高
  2. 空间中随后会产生出大量不连续的内存碎片

 

 

 

复制算法解决了标记-清除算法的两个不足,他的算法思想是将完整的内存划分为两等份,每次都只使用其中的一份,即整个内存空间的一半

当一份内存空间被用光以后,对这部分内存进行遍历,凡是不能被回收的对象,都完整复制到另一份内存中顺次存储起来,当整片内存复制完成后,将这一半的内存空间全部释放,然后集中使用另一块内存

 

进一步分析表明,新生代中,98%的对象的生命周期非常短暂,因此无需按照 1:1 划分内存,而是划分成一块较大的 Eden 空间和两块较小的 Survivor 空间,每次都使用 Eden 和两块 Survivor 中的一块,当他们的空间被占满以后,一次性的将不能被回收的对象全部复制到另一块空闲的 Survivor 空间中,清理掉刚刚使用的 Eden 和 Survivor 空间,这样可以将浪费的空闲空间降到很低,最初的 HotSpot 正是这么做的

 

 

然而,虽然改进后的策略降低了空间的浪费,可是却又带来了另一个问题,那就是有可能 Eden 与忙 Survivor 中不能被回收的内存空间大于另一块空闲的 Survivor 所能容纳的全部空间,这时,新生代就需要向老年代“借贷”一块内存,以解燃眉之急

 

上面的描述很明显可以得出一个结论,当对象生命周期很短,以至于每次内存回收都只有很少一部分内存不可以被回收时,复制算法是一个十分高效的算法,然而,对于老年代,这个算法就不怎么适用了

对于老年代,更适用于“标记-整理算法”,这个算法与标记-清除算法很像,区别在于在标记清除的同时,还需要让所有存活的对象向内存的一段移动,从而避免了内存碎片的产生

 

这种算法虽然效率较低,但是对于老年代这种非常不频繁的整理来说,整理的效率并不是什么问题

对于这种将内存划分为新生代和老年代,然后在不同的区域内运用不同的算法进行垃圾收集的算法也被称为“分代收集算法”,这在当前商业虚拟机中被广泛采用

 

HotSpot 对于新生代是使用可达性分析算法实现对象存活判定的

在 HotSpot 实现中,使用一组称为 OopMap 的数据结构存储对象内某个偏移对应的是什么数据,当对象加载完成,加载器就会将对象的这些信息存储到 OopMap,这样,在 GC 扫描时可以直接获取到这些信息

然而,由于很多指令都可以令 OopMap 发生变化,因此只有在被称为“安全点”的特定位置,才能够运行 GC,由于 jvm 是多线程模型,就会发生某些线程运行到安全点而另一些没有,如何让所有线程都在安全点上停顿下来,有两种方案可供选择:

  1. 抢先式中断 -- 在 GC 发生时,首先将所有线程全部中断,然后恢复没有运行到安全点的线程,直到他运行到安全点
  2. 主动式中断 -- 在 GC 发生时,对所有线程设置一个标志,每当线程运行到安全点,则检查该标志,如果标志位真,则自己主动中断挂起,这是当前 java 虚拟机的主要实现方式

 

虽然安全点的方法解决了如何进入 GC 的问题,但是对于 sleep 状态、blogcked 状态等线程没有被分给 CPU 的情况下,线程无法响应 jvm 的中断请求运行至安全点再挂起,这种情况就需要“安全区域”来解决

安全区域指的是在相应代码段中,任何时刻开始 GC 都是安全的

每当线程运行到安全区域,线程首先标识自己已经进入安全区域,这样 jvm 发起 GC 时,就不用管这样的线程了

 






读书笔记      技术帖      内存管理      技术分享      内存      memory      java      jvm      java虚拟机      hotspot      gc      垃圾回收      ibm     


京ICP备15018585号