深入理解JVM阅读笔记-对象引用
如何判断对象的可达性,如下提供了两种算法:
1.引用计数法
给对象添加一个引用计数器,每当有一个地方引用它的时候,计数器就加1;当引用失效的时候计数器就减1,任何时候计数器为0的对象都不可能再被使用。
引用计数法实现简单,判断效率高但是无法解决重复引用的问题。
如下重复引用代码:
/** * -verbose:gc -XX:+PrintGCDetails * @author scl * */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; private byte[] bigSize = new byte[2*_1MB]; public static void testGC(){ ReferenceCountingGC obj1 = new ReferenceCountingGC(); ReferenceCountingGC obj2 = new ReferenceCountingGC(); obj1.instance = obj2; obj2.instance = obj1; obj1 = null; obj2 = null; //执行GC System.gc(); } public static void main(String[] args) { testGC(); } }
如果使用的是引用计数法那么obj1和obj2的互相引用关系导致对象将不会被回收。
下方是GC信息:
[GC [PSYoungGen: 4742K->288K(37632K)] 4742K->288K(123520K), 0.0016883 secs]
这表示了将近4M的空间被回收了。那么证明了实际上虚拟机不是使用引用计数法来判定对象的存活性,而是可达性分析算法
2.可达性分析法
这个算法的基本思想是通过一系列被称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论来说的话就是从GC Roots到这个对象不可达)时,证明此对象是不可用的。如下图的object5,object6,object7虽然互有关联,但是它们到GC Roots是不可达的,所以它们会被判定为可回收对象。
在JAVA语言中,可作为GC Roots的对象包括以下几种:
1.虚拟机栈(栈帧中的本地变量表)中所引用的对象
2.方法区中类静态属性所引用的对象
3.方法区中常量所引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象
以上我们介绍了如何判定一个对象是否存活,无论使用引用计数法还是可达性分析法最终都是要判定对象是否真实的被引用。在JDK1.2之前对于对象的引用只存在两种状态:对象被引用和对象没有被引用。但是这样就没有办法定义对象缓存(在内存允许的情况下对象可以保留在内存中,当内存不够进行垃圾回收的时候抛弃这些对象)。所以JDK1,2之后对于对象的引用进行了扩充,将引用分为强引用,软引用,弱引用,虚引用。
强引用(Strong Reference):
代码中普遍存在的“”Object o = new Object()” 这类的引用,只要强引用还在那么GC将永远不会回收强引用的对象。
软引用(Soft Reference):
描述一些还有用但是并非必须的对象,对于软引用关联的对象,在系统即将发生内存溢出之前,将会将这些对象列入回收范围再进行一次回收,如果这次回收还没有获得足够的内存,那么才会抛出内存溢出异常。
弱引用(Weak Reference):
也是描述一些有用但是非必须的对象,但是生存周期会短一些,被弱引用关联的对象只会生存到下次GC之前,GC后无论内存是否充足等都会回收掉当前的弱引用
虚引用(Phantom Reference):
对于对象的生存周期没有任何影响,为一个对象设置一个虚引用的目的仅仅是为了在对象被回收的时候收到一个系统通知。也被称为幽灵引用或者幻影引用。
对象在不可达之后的清除问题-finalize
即使对象在可达性分析法下判定为不可达对象,那么对象也不是立刻被回收的,要判定一个对象死亡需要经过两次标记过程:第一次标记过程是筛选对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或者对象的finalize方法已经执行过的情况下,虚拟机将这两种情况都视没有必要执行,如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被放置在一个F-Quence的队列之中,并在稍后由虚拟机自动建立的,低优先级的Finalizer线程去执行它,而且虚拟机只保证会触发finalize方法,但不承诺会保证等待它运行结束。这样做的原因是防止一个对象的finalize方法执行缓慢,或者发生了死循环之后,将会导致F-Quenc队列中的其它对象永久处于等待状态,甚至导致GC崩溃。Finalize方法是对象逃离被回收的最后一次机会,如果对象要在finalize中成功的救自己,那么只需要重新和引用链的任何一个对象建立关联即可,比如将自己(this)赋值给某个类的变量或者成员的成员变量,那么在第二次标记的时候它就可以被移除回收集合,如果一个对象在执行完finalize还处于不可达状态,那么它就只有等待被回收了。
以下是示例代码:
public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive(){ System.out.println("yes,i am still alve"); } @Override protected void finalize() throws Throwable { // TODO Auto-generated method stub super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次执行finalize方法 SAVE_HOOK = null; System.gc(); //应为finalize方法执行的优先级很低,所以需要暂停0.5秒等待 Thread.sleep(500); if(SAVE_HOOK !=null){ SAVE_HOOK.isAlive(); }else{ System.out.println("no,i am dead "); } //第二次执行GC SAVE_HOOK = null; System.gc(); Thread.sleep(500); if(SAVE_HOOK !=null){ SAVE_HOOK.isAlive(); }else{ System.out.println("no,i am dead "); } } }
执行结果如下:
finalize method executed!
yes,i am still alve
no,i am dead
上方执行结果表示对象在GC回收执行会执行finalize方法,而且在该方法内只要重新存在引用就可以继续存活,而且finalize方法只会执行一次的特点,所以第二次同样的代码才会获得不同的执行结果。虽然finalize方法可以拯救对象,但是不推荐使用。它的运行代价高昂,不确定性大,无法保证各个对象的执行顺序,如果需要完成关闭外部资源的动作,那么finally会更适合一些,所以完全可以忘记finalize这个方法的存在。