常见 GC 算法思想
2016-03-16 22:18:31 最后更新: 2016-03-17 13:33:05 访问数量:1518
2016-03-16 22:18:31 最后更新: 2016-03-17 13:33:05 访问数量:1518
如图所示,标记-清除算法是垃圾收集算法中最基础的一个,顾名思义,这个算法分为标记和清除两个阶段,在标记阶段,通过对内存区域的遍历,对需要回收的对象进行标记,然后在清除阶段将已经被标记的对象进行清除
在上一篇日志中,我们说过,事实上这个标记过程需要进行两次标记,从而选出没有 finalize 方法或者已经被执行了 finalize 方法的对象
这个算法有两个不足:
复制算法解决了标记-清除算法的两个不足,他的算法思想是将完整的内存划分为两等份,每次都只使用其中的一份,即整个内存空间的一半
当一份内存空间被用光以后,对这部分内存进行遍历,凡是不能被回收的对象,都完整复制到另一份内存中顺次存储起来,当整片内存复制完成后,将这一半的内存空间全部释放,然后集中使用另一块内存
进一步分析表明,新生代中,98%的对象的生命周期非常短暂,因此无需按照 1:1 划分内存,而是划分成一块较大的 Eden 空间和两块较小的 Survivor 空间,每次都使用 Eden 和两块 Survivor 中的一块,当他们的空间被占满以后,一次性的将不能被回收的对象全部复制到另一块空闲的 Survivor 空间中,清理掉刚刚使用的 Eden 和 Survivor 空间,这样可以将浪费的空闲空间降到很低,最初的 HotSpot 正是这么做的
然而,虽然改进后的策略降低了空间的浪费,可是却又带来了另一个问题,那就是有可能 Eden 与忙 Survivor 中不能被回收的内存空间大于另一块空闲的 Survivor 所能容纳的全部空间,这时,新生代就需要向老年代“借贷”一块内存,以解燃眉之急
上面的描述很明显可以得出一个结论,当对象生命周期很短,以至于每次内存回收都只有很少一部分内存不可以被回收时,复制算法是一个十分高效的算法,然而,对于老年代,这个算法就不怎么适用了
对于老年代,更适用于“标记-整理算法”,这个算法与标记-清除算法很像,区别在于在标记清除的同时,还需要让所有存活的对象向内存的一段移动,从而避免了内存碎片的产生
这种算法虽然效率较低,但是对于老年代这种非常不频繁的整理来说,整理的效率并不是什么问题
对于这种将内存划分为新生代和老年代,然后在不同的区域内运用不同的算法进行垃圾收集的算法也被称为“分代收集算法”,这在当前商业虚拟机中被广泛采用
HotSpot 对于新生代是使用可达性分析算法实现对象存活判定的
在 HotSpot 实现中,使用一组称为 OopMap 的数据结构存储对象内某个偏移对应的是什么数据,当对象加载完成,加载器就会将对象的这些信息存储到 OopMap,这样,在 GC 扫描时可以直接获取到这些信息
然而,由于很多指令都可以令 OopMap 发生变化,因此只有在被称为“安全点”的特定位置,才能够运行 GC,由于 jvm 是多线程模型,就会发生某些线程运行到安全点而另一些没有,如何让所有线程都在安全点上停顿下来,有两种方案可供选择:
虽然安全点的方法解决了如何进入 GC 的问题,但是对于 sleep 状态、blogcked 状态等线程没有被分给 CPU 的情况下,线程无法响应 jvm 的中断请求运行至安全点再挂起,这种情况就需要“安全区域”来解决
安全区域指的是在相应代码段中,任何时刻开始 GC 都是安全的
每当线程运行到安全区域,线程首先标识自己已经进入安全区域,这样 jvm 发起 GC 时,就不用管这样的线程了
欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤