如何判断对象是否已死
在堆里存放着几乎所有的对象实例,垃圾收集器在堆进行回收前,要判断对象是否还存活着。
Java堆是虚拟机所管理的内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动创建。此内存区域唯一目的就是存放对象实例。
引用计数法(Reference counting)
在对象中添加一个计数器,每当一个地方引用它时,计数器值就加1,当引用失效时,就减一,任何时刻计数器为零的对象就不可能再被使用的。
引用计数法虽然占用额外的内存空间进行计数,但是原理简单,判定效率也很高,大对数情况下都是一个不错的算法,但是在Java领域主流的虚拟机没有想用此算法管理内存,需要很多例外的情况需要考虑,譬如对象之间的相互引用的问题。
举个例子,下面代码中的testGC()方法,对象ObjA和ObjB都有instance,相互赋值,除此之外两个对象再无其他引用,实际上这两个对象已经不能在访问了,但是因为他们互相引用着对方,导致他们的引用计数都不为零,引用技术算法无法回收他们。
/**
* @Author: www.ipooli.com
* @DateTime: 2020/9/2 14:57
* @Description:
* ObjA 和 ObjB都会被回收掉,不会因为互相引用而放弃回收。
*
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 只为占用内存,以便在GC日志看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
运行结果:
[GC (System.gc()) [PSYoungGen: 7997K->840K(75776K)] 7997K->848K(249344K), 0.0012598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 840K->0K(75776K)] [ParOldGen: 8K->630K(173568K)] 848K->630K(249344K), [Metaspace: 3122K->3122K(1056768K)], 0.0043636 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 75776K, used 1951K [0x000000076b600000, 0x0000000770a80000, 0x00000007c0000000)
eden space 65024K, 3% used [0x000000076b600000,0x000000076b7e7cc0,0x000000076f580000)
from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
to space 10752K, 0% used [0x0000000770000000,0x0000000770000000,0x0000000770a80000)
ParOldGen total 173568K, used 630K [0x00000006c2200000, 0x00000006ccb80000, 0x000000076b600000)
object space 173568K, 0% used [0x00000006c2200000,0x00000006c229db18,0x00000006ccb80000)
Metaspace used 3137K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
可以看到7997K->840K ,意味着虚拟机并没有因为互相引用而放弃回收,也侧面说明虚拟机并不是通过引用计算法来判断对象的存活。
可达性分析算法(Reachability Analysis)
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的。
这个算法的基本思路就是通过一系列成为 "GC Roots" 的根对象作为起始节点集,从这些节点开始,
根据引用关系向下搜索,搜索过程所走过的路径成为"引用链",如果某个对象到GC Roots间没有任何引用链相连,
或者说从GC Roots到这个对象不可达时,则证明对象不可能再被使用的。
如图所示,对象object5,object6,object,7 虽然互有关联,但是它们到GC Roots是不可达的,因此它们会被判定为可回收的对象。
GC Roots 对象包括以下几种
- 在虚拟机栈中引用的对象,比如各个线程被调用的方法堆栈中使用的参数,局部变量,临时变量等。
- 在方法区中类静态属性引用的对象。
- 在方法区中常量引用的对象,比如字符串常量池里的引用
- 在本地方法中JNI(Native方法)引用的对象
- Java虚拟机内部的引用
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等。
但是即使在可达性分析算法中判定为不可达的对象,也不是非死不可
的,这时候会被第一次打上标记,随后在进行一次筛选,筛选的条件为此对象时候有必要执行finalize()方法。
假如对象没有覆盖finalize()方法,或者已经调用过了,那么虚拟机将这两种情况都视为"没有必要执行"。 如果这个对象被判定有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的,低调度优先级的Finalize线程去执行它们的finalize()方法。
但不会等待它运行结束。稍后收集器会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法成功拯救自己——只要重新与任何GC Roots建立关联即可。那么在第二次标记是它会被移出"即将回收"的集合,如果对象还没逃脱,那基本上它就真的要被回收了。 从如下代码中可以看到,对象执行finalize()方法,但是它仍然可以存活。
/**
* @Author: www.ipooli.com
* @DateTime: 2020/9/2 15:34
* @Description:
* 对象可以在被GC的时候自救
* 任何对象的finalize 只会被调用一次,如果对象面临下次的回收将不会被执行。
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes,i'm still alive");
}
/**
*
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no,i'm dead");
}
//下面这段代码与上面完全相同但是这次却自救失败了
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no,i'm dead");
}
}
}
运行结果:
finalize method executed!
yes,i'm still alive
no,i'm dead
从结果可以看到 SAVE_HOOK对象的finalize()方法确实被垃圾收集器触发过,并且在被收集前成功逃脱了。 但是下面那段代码却自救失败了,这是一位任何一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码自救失败了。
但是尽量避免使用finaliz()方法,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方声明为不推荐使用的语法。
总结
虚拟机通过可达性分析算法判定对象是否存活,对象也还有一次通过finalize()方法来自救,但不建议使用finalize()语法。
参考《深入理解Java虚拟机(第3版)》: 《深入理解Java虚拟机(第3版)》