本文是《深入理解Java虚拟机》3.2节的读书笔记,理解有误的地方欢迎指正
如何判断对象是否可以回收,传统的做法是引用计数法:
引用计数算法:给对象添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻引用计数器值为0的对象就是不可能再被使用的。
引用计数算法看起来简单明了,但是却存在一个麻烦的问题——即循环引用。假设现在我使用引用计数算法回收对象,有如下代码正在执行:
public class Test {
public Object instance = null;
public static void main(String[] args){
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
//...
}
}
当我想回收对象a的时候,发现它被对象b引用,那么要使它的引用计数为0,必须先干掉对象b。但是当我掉头去准备先干掉对象b的时候,却发现它被对象a引用,于是要干掉b必须先干掉a。。。如此,陷入了一种死锁的状态。最后,在整个程序执行期间,对象a和b都得不到回收,造成内存泄漏。
由于循环引用等问题,主流Java虚拟机中没有选用引用计数算法来管理内存,而是使用 可达性分析算法 。
可达性分析算法:通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
Java语言中,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性(static修饰)引用的对象
- 方法区中常量(final修饰/字符串常量)引用的对象
- 本地方法栈中JNI(即Native方法)引用的对象
当通过可达性分析算法发现对象不可达时,并不能就此宣告对象死亡,宣告一个对象的死亡至少要经过两次标记过程:
当第一次发现对象通过 GC Roots 不可达时,该对象将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。(当对象实现了finalize()方法并且该方法未被虚拟机调用过,则判定该对象有必要执行finalize()方法)。
无必要执行finalize()方法的对象等待下一次标记的时候宣告死亡。有必要执行finalize()方法的对象被放入F-Queue队列中,该队列将由虚拟机自动建立的、低优先级的Finalizer线程执行。
对象可以在finalize()方法中将自己重新与 GC Roots 引用链建立联系,这样在下一次标记时,就会被移出 “即将回收” 的集合。否则,二次标记后就可以被回收了。