5.7 垃圾回收算法
前边都是在讲如何判断一个对象是否要被回收,现在要开始讲如何回收一个对象了。
进行垃圾回收主要要做两件事情:
- 找到内存中存活的对象(之前讲的可达性分析算法)
- 释放不再使用的对象的内存,使得程序能够再次使用这部分空间
垃圾回收算法的分类:
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代GC
垃圾回收算法的评价标准: Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有用户线程,这个过程被称为Stop The World(STW)
。如果STW过长,这个垃圾回收算法就比较差了。
不同的垃圾回收算法,适用于不同的场景。没有最好的垃圾回收算法,只有最适合的垃圾回收算法。
----常用垃圾回收器参数----
5.8 垃圾回收算法的原理
(1)标记清除算法
第一个阶段,标记阶段,将所有存活的对象进行标记,Java中主要使用可达性分析算法,从GC Root通过引用链遍历出所有存活对象。
第二个阶段,清除阶段,从内存中删除没有被标记也就是非存活对象。
优缺点:
- 优点:实现简单
- 缺点:
- 碎片化问题。每次回收多个对象,但是这些对象在内存中分布不一定是连续的,会让内存变得支离破碎,当想要申请一块比较大的连续内存时,会发现内存中都是不连续的小内存。
- 分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,很有可能出现每次需要遍历到链表的最后才发现可以获得合适的内存空间。比如说,空闲链表的最后一个元素是一个1k字节的内存,其他位置都是100字节的内存。当需要用到的内存大于100字节小于1k字节时,需要遍历到链表尾部才能找到一块合适的内存。
(2)复制算法
准备两块内存空间From空间和To空间,每次在对象分配阶段,只能使用From空间。在垃圾回收阶段,将From中存活对象复制到To空间,接下来清理From空间。最后将From空间和To空间的名字互换。(每次创建都在From中创建,要回收了,就把From中还有用的放到To,然后把From一窝端,互换From和To的名字。接着就可以继续往From中创建对象了)
存活对象指的是与GC Root关联的对象。
优缺点:
(3)标记整理算法
这个算法是对标记清除算法中容易产生内存碎片的一种解决方案。
- 标记,与标记清除算法一致。
- 整理,将存活对象移动到堆的一端,此时非存活对象会在堆的另一端,清除掉非存活对象。
优缺点:
(4)分代GC
分代GC,混合了上述垃圾回收算法。
分代GC将整个内存区域划分为两个部分——新生代(Young)和老年代(Old)。新生代中存放存活时间短的对象,老年代中存放存活时间比较长的对象。新生代中划分为Eden区和幸存区(S0)和S1区。S0和S1区其实就是复制算法中的From和To。
分代GC的算法步骤:
创建出来的对象首先会被放入Eden区。
随着对象在Eden区越来越多,如果Eden区满,新创建对象已经无法放入,会触发年轻代的GC,称为Minor GC或者Young GC。Minor GC会把Eden中和From中不需要回收的对象放入To区,清理需要回收的对象,然后互换From和To名字。(这一部分跟前边的复制算法是一样的)
注意:每次Minor GC会增加未被清理的对象的年龄,如果对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
把对象晋升老年代有个好处,频繁将对象从From复制到To会耗费性能,对象晋升老年代可以减少这部分对象的性能耗费。
当整个新生代的空间都被使用时,先尝试Minor GC,把未达到年龄阈值但是最老的对象放入老年代,如果老年代空间也被占满了,会触发Full GC(尝试回收整个堆的对象,老年代中的对象会被清理)。
如果Full GC依然无法回收老年代中的任何对象,就会抛出OutOfMemory异常。
为何分代GC要将堆分为新生代和老年代,这种设计的原因是什么?