序
垃圾回收机制并不是Java语言特有的产物,甚至其思想产生比Java历史更久远,直至Java的兴起才将这门“自动化”内存管理技术发扬光大。回忆一下Java内存运行时的各个区域,不熟悉的同学请移步JVM内存模型-详解。其中程序计数器,虚拟机栈,本地方法栈这3个区域是跟线程的生命周期保持一致的,空间的分配和回收都是具有确定性的,线程结束,内存自然可以跟着回收了,所以这些区域不用过多关注内存的回收。但是对于Java堆和方法区却天然存在不确定性,只有运行期间我们才知道创建了多少对象,哪个程序片段会创建对象,所以这部分内存才是JVM垃圾回收机制所关注的区域。
判定对象是否存活
既然我们想要回收内存,那么首先要确认的就是哪些对象可以回收,哪些并不能回收,即我们要找到“存活”的对象,或者找到已经“死亡”的对象,简单来收,就是回收没有任何引用的对象,并保留还在使用中的对象。通常的,对于判单对象是否存活,我们会提到两个算法:引用计数算法和可达性分析算法。
对引用概念不清楚的同学可以移步:Java中的引用类型-简述
引用计数算法
简单来说,引用计数法(Reference Counting)的思想就是在对象中引入一个引用计数器,当有新的引用时,计数器加一,当引用失效时,计数器减一。为零时则表明该对象没有被使用,即对象已“死亡”,可以安全回收。虽然该算法需要有额外的计数器内存开销,但是其原理简单,判定的效率也高。经典使用案例,例如微软的COM,Flash Player,Python等使用了该算法来进行内存管理。但是在Java领域,主流的JVM都没有采用这种算法,一个很主要的原因就是简单的引用计数算法无法解决循环依赖问题。如下图:
其实这两个对象已经没有其他对象在使用了,但是由于计数器数值不为零,导致判定为存活,造成内存浪费。
下面我们来看一段代码:
public class GCTest {
GCTest ref = null;
/**
* 加大对象内存占用,以便于观察
*/
private static final int oneMB = 1024 * 1024;
private byte[] arr = new byte[oneMB * 2];
public static void testGC() {
GCTest A = new GCTest();
GCTest B = new GCTest();
A.ref = B;
B.ref = A;
A = null;
B = null;
// 调用GC观察
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
注:idea中观察GC日志请添加虚拟机参数:-XX:+PrintGCDetails
运行结果:
可以看到,并没有因为对象A和对象B的循环引用,而放过清理,这也证明了JVM未采用引用计数法来判定对象是否死亡。
可达性分析算法
基本思想:可达性分析算法(Reachability Analysis)通过一系列“GC Roots(根对象)”作为起始节点集,根据引用关系向下搜索,称搜索走过的路径为“引用链”(Reference Chain),若对象跟GCRoot之间不存在引用链的话,则判定该对象可回收,用图论的方式来描述就是该对象不可达。如下图:
可以清楚看到,虽然obj6、obj7、obj8之间有引用也会被判定为死亡。
在Java中一般作为GC Roots的对象包括:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中,Native方法引用的对象。
- JVM内部的引用。
- 被同步锁持有的对象。
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除去以上固定的GC Roots集合外,也可以临时加入其他对象,这跟选择的收集器以及收集的区域有关。
小结
本文简单描述了垃圾回收的前期准备,即判断对象是否可回收的两种算法。这将为后期垃圾收集算法打下基础。