对象是否存活
引用计数算法:一般判定对象是否存活的方式是通过引用计数器来记录和判定的。其过程大概是记录一个对象被引用的次数,当增加这个对象的引用时就将引用次数加1;当一个引用失效时就将引用次数减1,当对象的引用次数为0时就回收这个对象。这种方式简单且方便(有许多语言也都使用了这种方式,如Python),但是它不能解决对象间的循环引用问题。
可达性分析算法:Java中是通过可达性分析(Reachability Analysis)来判定对象是否存活的。它会选取一系列的对象作为“GC Roots”,然后判定这些GC Roots和其他对象之间的可达性,如果对象不可达,则说明这个对象不可用,即可被回收。
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即Native方法)引用的对象。
垃圾收集算法
标记-清除算法(Mark-Sweep):该算法分为两个阶段,即标记和清除。它首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:标记和清除的效率都不高;无法充分利用碎片化内存。
复制算法(Copying):将可用内存按照容量分为大小相等的两块,每次只使用其中一块,当这一块内存用完时,就将还存活着的对象复制到另外一块上面,然后把已经使用过的内存空间一次清理掉。这种算法每次可以回收整个内存的一半,且实现简单运行高效,但是其代价为缩小内存的一半,故代价太高。JVM中的新生代一般会采用这种办法进行内存管理,但是它将内存分为一份Eden和两份Survivor,然后按照8:1:1(默认,但是可以调整)的方式进行分配,每次使用一份Eden一份Survivor,回收时将这两个中存活的对象收集到另外一份Survivor中,然后继续使用这另外一份Survivor和Eden空间作为新的内存空间。当Survivor空间不足时需要依赖其他内存进行分配担保。
标记-整理算法(Mark-Compact):其标记过程和“标记-清理算法”一致,但是后续不是直接对可回收内存进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:将Java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。新生代回收时会有大量死亡对象,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,没有额外空间对它进行担保,就必须用“标记-清理”或者“标记-整理”算法进行回收。
Java虚拟机中依据这些原理实现了Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old 、CMS(concurrent Mark Sweep)、G1等垃圾回收器,这些垃圾回收器各自拥有各自的优势,它们的关注点各不相同,因此只能根据各自业务的需要不同,选择不同的垃圾回收器。Serial只使用一个线程进行垃圾回收,在进行垃圾回收时必须停顿所有其他工作线程;ParNew是Serial的多线程版本,可以使用多个线程进行垃圾回收;Parallel Scavenge是新生代收集器,它可以提供可控的吞吐量(可手工或自动化配置最大垃圾停顿时间或者吞吐量);Serial Old是Serial的老年代版本,它也是单线程的收集器;Parallel Old是Parallel Scavenge的老年代版本,是多线程收集器;CMS是一种以获得最短回收停顿时间为目标的收集器,它比较重视相应速度,一般适合于Java网站应用或者B/S系统的服务器上;G1是最新的收集器。