概述
垃圾收集器(Garbage Collection, GC)的历史要比Java久远,且并非Java独有,GC主要完成以下三件事情:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
对于Java内存运行时区域的各个部分,程序计数器、虚拟机栈、本地方法栈3个线程私有区域是随线程而生,又随线程而灭,因此这几个区域的内存分配和回收都具备确定性,不需要考虑垃圾回收的问题。而Java堆和方法区这两个线程共享区的内存是动态分配的,因此垃圾收集器主要关注的是这部分的内存,这里也是垃圾回收的主战场。
如何判断对象死亡
引用计数算法
原理:给对象添加一个引用计数器,当有一个地方引用它时,计数器加1,当引用结束时,计数器减1,任何时候当引用计数为0时,则认为该对象没有被使用,可以被回收。
引用计数算法实现简单,效率也很高,也有很多技术中都使用该算法来管理内存。但Java虚拟机却没有使用该算法,最主要的原因它很难解决对象之间互相循环引用的问题。例如下面示例代码:
public class ReferenceCountingGC {
public Object instance = null;
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
上述代码中objA与objB互相引用,如果Java虚拟机采用引用计数算法,发生GC时,objA、objB将无法被回收。测试可知,上述Java代码在GC时是可以回收内存的,因此可从侧面证明Java虚拟机并非用的引用计数算法。
可达性分析算法
其实在Java、C#等主流语言中,使用的都是可达性分析(Reachability)算法来判定对象是否存活的。该算法基本思路是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,用图论原理来说,即GC Roots到这个对象是不可达的,则以此判断该对象是不可用的。
可达性分析原理示意图(来自Google I/O)
在Java语言中,可作为GC Roots的对象有以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
常用的垃圾收集算法
标记-清除算法
标记-清除(Mark-Sweep)算法是最基础的垃圾收集算法,其分为两个阶段:首先标记出所需要回收的对象,然后在标记完成后统一回收所有被标记的对象。
标记-清除算法的不足:
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后要为大对象分配内存时,如果找不到足够的连续内存而不得不触发另一次垃圾收集动作。
标记-清除算法原理示意图(来自《深入理解Java虚拟机》)
复制算法
复制(Copying)算法为了解决上述算法的效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清空。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法原理示意图(来自《深入理解Java虚拟机》)
现代商业虚拟机都采用这种收集算法来回收新生代,但按上述原理,将内存缩小为原来的一半使用,这种代价有点太大了。IBM研究证明,98%的对象是“朝生夕死”,所以不必按1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor。当回收内存时,将Eden和第一块Survivor中还存活的对象一次性地复制到第二块Survivor中,然后清理掉Eden和第一块Survivor空间。HotSpot虚拟机默认Eden与Survivor空间的大小比例为8:1,一旦第二块Survivor不足以容纳Eden与第一块Survivor复制过来的存活对象时,这些对象将通过分配担保机制进入老年代。
标记-整理算法
复制收集算法在对象存活率较高时就要频繁进行复制操作,导致效率变低,因此在老年代一般不选用这种算法。根据老年代的特点,有人提出了标记-整理(Mark-Compact)算法,标记过程跟标记-清除算法的一样,但后续步骤不是直接将可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-整理算法原理示意图(来自《深入理解Java虚拟机》)
分代收集算法
当前虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法。算法思想是跟根据对象存活周期的不同将内存划分为几块,一般将Java堆划分为新生代和老年代,这样可以根据各个年代的特点采用最合适的垃圾收集算法。
在新生代中,每次垃圾收集时都有大量对象死亡,只有少量存活,因此优选复制算法,这样只需要付出少量对象的复制成本就可以完成收集;而对于老年代,因为对象存活率高,且没有额外的空间对它进行分配担保,就更适合使用标记-清理或标记-整理算法来进行回收。