前情提要,当内存空间不足的时候,JVM 就会触发垃圾回收机制,对垃圾对象进行回收,清理出足够的内存空间,存放新的对象。那么,JVM 是怎么识别垃圾对象的?判断的标准是什么?接下来,让我们一起带着问题,去寻找答案吧!
何为垃圾?没用的、不需要的东西就是垃圾。
在代码的世界也是如此,当对象 不被引用 的时候,我们就可以认为,这就是一个垃圾对象。那么,有什么办法可以识别哪些对象不被引用呢?
最简单办法,就是对实例对象被引用的次数做一个记录,具体实现方式就是在对象头中增加了一个计数器属性。
◉ 当有一个引用指向实例对象,则引用计数器+1。
◉ 当有一个引用不再指向实例对象,则引用计数器-1。
◉ 当实例对象的引用计数器为0,那么它就是垃圾对象。
这个方法很简单,标记垃圾对象的效率高。
然而,引用计数有一个致命的缺点:循环引用,即实例对象的引用链形成闭环。这种情况,相当于我捉住你,你捉住我,我们彼此都不松开,只能一直维持现状,停留在原地,哪儿都去不了。
这导致实例对象的引用计数器永远都不为 0,意味着实例对象永远都不会被标记为垃圾对象,所占有的内存得不到释放,这就产生了 内存泄露。
下面用一段代码,进行例子说明:
// 创建实例A,且 a 引用实例A // 实例A的引用计数+1 = 1 A a = new A(); // 创建实例B,且 b 引用实例B // 实例B的引用计数器+1 = 1 B b = new B(); // a.instance 实际上是引用实例B // 实例B的引用计数器+1 = 2 a.instance = b; // b.instance 实际上是引用实例A // 实例A的引用计数器+1 = 2 b.instance = a; /** 至此,实例A 和 实例B 形成循环引用 **/ // 当 a 不再引用实例A // 实例A的引用计数器-1 = 1 a = null; // 当 b 不再引用实例B // 实例B的引用计数器-1 = 1 b = null;
至此,实例A 和 实例B 的引用计数都不为0,而且,由于两个实例依然存在彼此的引用 且 无法取消引用,那么两个实例的引用计数器都无法归零,因此在采用引用计数法的场景下,两个实例占有的内存都将得不到释放,造成了内存泄漏。
配合两张内存空间的图片,方便大家理解。
图1(实例A 和 实例B 形成循环引用)
图2(a不再指向实例A、b不再指向实例B)
目前主流的虚拟机采用的都是 可达性算法(GC Roots Tracing),这个算法的核心是利用一系列 根对象(GC Roots )作为起始点,根据对象之间的引用关系搜索出一条 引用链(Reference Chain),通过 遍历 引用链来判断对象的是否存活。
如果对象不在任何一条 引用链,即这个对象没有被任何一个 GC Roots 相连,说明这个对象 不可达,那么将会被判定为可回收的垃圾对象。
举个例子,假设 GC Roots 就是迷宫的起点,每个实例对象 就是一块区域,引用链 就是通往各个区域的路线。那些没有任何一条路线可以到达的区域就是可回收的垃圾对象,因为不能到达的区域,并没有任何的意义,只能是浪费空间罢了。
那么,GC Roots 是什么呢?
◉ 虚拟机栈(栈帧的局部变量表)中的引用。
◉ 方法区中类静态属性引用。
◉ 方法区中常量引用。
◉ 本地方法栈JNI(Native方法)引用。
下面分别用几段代码,进行例子说明:
虚拟机栈(栈帧的局部变量表)中的引用
public class Demo { public static void main(String[] args) { // 创建实例A // a1:就是虚拟机栈(栈帧的局部变量表)中引用 A a1 = new A(); // 当 a1 不再指向实例A的时候,实例A 将不可达 a1 = null; } }
方法区中类静态属性引用
public class Demo { // 创建实例A // a2:方法区中类静态属性引用 public static A a2 = new A(); public static void main(String[] args) { // 当 a2 不再指向实例A的时候,实例A 将不可达 a2 = null; } }
方法区中常量引用
public class Demo { // 创建实例A // a3:方法区中常量引用 public static final A a3 = new A(); public static void main(String[] args) { // 当 a3 不再指向实例A的时候,实例A 将不可达 a3 = null; } }
本地方法栈JNI(Native方法)引用
// 访问 java 的构造方法 JNIEXPORT jobject JNICALL Java_com_test_Demo_accessConstructor (JNIEnv * env, jobject jobj) { // 通过类的路径从 JVM 找到 A 类 // a4:本地方法栈JNI(Native方法)引用 jclass a4 = (*env)->FindClass(env, "java/test/A"); jmethodID jmid = (*env)->GetMethodID(env, jc, "<init>", "()V"); // 通过 NewObject 实例化,创建实例A jobject date_obj = (*env)->NewObject(env, jc,jmid); return date_obj; }
配合一张内存空间的图片,方便大家理解。
图3
以上的内容,简单讲述了 JVM 标记垃圾对象的两个方法:引用计数法 和 可达性算法。其中,引用计数法 我们只需要简单了解,可达性算法 才是目前主流虚拟机所采用的算法。
然而,可达性算法 在真正实现的场景下,有什么值得深入了解的地方呢?例如:如何枚举 GC Roots?GC Roots 向下遍历的优化?并发场景下标记对象会有存在什么问题?学无止境,这里挖一个坑,后面将会带大家深入了解这些问题,尽情期待。