对象死了吗
垃圾收集器的目的就是回收掉虚拟机内存中无用的对象,而这些无用的对象我们称为“死掉的对象”。那么怎么判断一个对象已经“死掉”,即怎么判断一个对象是无用对象呢?
引用计数算法(Reference Counting)
就是给每个对象添加一个引用计数器,每当一个地方引用这个对象,计数器的值就加1,引用失效计数器就减1。那么,当计数器的值为0时,这个对象就是无用对象了,已而为没有引用指向这个对象,也就没办法再使用对象了。
这种垃圾收集器算法实现简单,判定效率高,但是有一个问题:无法处理对象相互引用的情况。这种情况下,相互引用的对象的引用计数器值都大于0,实际上相互应用的对象已经不能再被访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class ReferenceCountingGC { public static void main(String[] args) { testGC(); } 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; // 假设在这行发生GC,那么objA和objB是否能被回收? System.gc(); } } |
Java虚拟机不是通过引用技术算法来判断对象是否存活的,所以这个例子中,对象依然被回收了。
可达性分析算法(Reachablility Analysis)
在Java、C#等主流商用编程语言的实现中,都是使用可达性分析来判断对象是否存活的。这个算法的基本思路是:通过一系列成为“GC Roots”的对象作为起始点,从这些起始点开始搜索,通过引用链(Reference Chain)搜寻所有可达的对象,当一个对象到GC Root没有任何引用相连,证明此对象不可用,即为可回收对象。
在Java中,可作为GC Roots的对象包括下面几种:
– 虚拟机栈(栈帧本地变量表)中引用的对象
– 方法区中类静态属性引用的对象
– 方法区中常量医用的对象
– 本地方法栈中JNI(即一般说的Native方法)引用的对象
引用的类型
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度一次逐渐减弱。
– 强引用:只要引用在,垃圾收集器就不会回收掉被引用的对象,如Object obj = new Object()
。
– 软引用:描述一个非必须的对象,在系统将要发生内存溢出异常之前,进行二次回收,即使这个引用还存在。使用SoftReference
类来实现。
– 弱引用:也是用来描述一个非必须的对象,但是被弱引用关联的对象只能生存到下一次垃圾收集之前。使用WeakReference
类来实现。
– 虚引用:被虚引用关联的对象,不会和没有引用关联的对象在生存时间上有任何区别,也无法通过虚引用来取得对象实例,使用虚引用的唯一目的就是这个对象在被收集器回收时能收到一个系统通知。使用PhantomReference
类来实现。
关于finalize()方法
即使可达性分析后不可达对象也不一定会执行“死刑”,要宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法。当对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过,即为“没有必要执行finalize()
”。
如果这个对象被判定有必要执行finalize()
方法,那么这个对象将会放置在一个叫做F-Queue的队列中,稍后会由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,这里的执行仅仅是“触发”,而不保证finalize()
方法执行完成,因为如果某个对象的finalize()
在执行的过程中发生了死循环等极端状况,那么虚拟机不可能永久等待它执行完成。对象可以再finalize()
中进行“自救”,即重新与引用链建立关系,那再第二次标记时,它将被移除“即将回收”集合,剩下在集合中的就被回收了。fianlize()方法只会被虚拟机执行一次,当对象再次与引用链断开时,就不能再通过finalize()
进行自救了。
我们来看个对象自救的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/** * 这个例子讲述了一个濒死的对象时如何得到救赎的 * 对象在第一次GC时被判处死刑,但是它在finalize()方法中进行了申诉,抓住了一根救命稻草 * 但是main()判定这根救命稻草无效(null),这个对象最终还是被执行了死刑 * 这是个悲伤的故事:p * @author WangHeng * */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("Yes!, I am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("NOT TODAY!"); //抓住一根救命稻草(GC Root) FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); SAVE_HOOK = null; //宣布对象的死刑 System.gc(); Thread.sleep(500);//可以再0.5s内进行申诉 if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("I'm sorry, I tried my best.:("); } SAVE_HOOK = null; //驳回上诉,维持原判 System.gc(); Thread.sleep(500);//可以再0.5s内进行申诉,但最高法院不会受理的 if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("I'm sorry, I tried my best.:("); } } } |
执行的结果:
NOT TODAY!
Yes!, I am still alive
I’m sorry, I tried my best.:(
对于finalize()
,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有些教科书中描述它适合做“关闭外部资源”之类的工作,这完全是对这个方法用途的一种自我安慰。
忘掉
finalize()
吧。
方法区的卡机回收
方法区的垃圾收集在Java虚拟机规范中是不要求实现的。但是在大量使用反射、动态代CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader
的场景都需要虚拟机具备类卸载的功能。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
废弃常量:如果没有任何对象引用常量池中的常量,或者也没有任何其他地方引用了一个字面量,此时发生内存回收,并且有必要的话,则常量被清理出常量池。
无用的类:同时满足下面3个条件
– 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
– 加载该类的ClassLoader
已经被回收。
– 该类对应的java.lang.Class
对象没有任何地方被引用,无法再任何地方通过反射访问该类的方法。
满足以上3个条件的无用类仅仅是“可以”被回收,是否被回收,HotSpot虚拟机提供了如下参数:
– -Xnoclassgc
控制不进行回收
– -verbose:class
以及-XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息(-verbose:class
和-XX:+TraceClassLoading
可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading
需要FastDebug版的虚拟机)