介绍了Java中的垃圾分析算法,包括引用计数法和可达性分析算法的原理!
在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete关键字释放内存资源。如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,最终可能会导致内存溢出。
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。
不可能再被任何途径使用的对象,便可称之为垃圾,就可以被回收了。常见的垃圾分析算法有两种,一种是引用计数法,另一种是可达性分析算法。
引用计数是最简单直接的一种方式,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,那么此对象就可以作为垃圾收集器的目标对象来收集。
如下案例:
public class ReferenceCountingGC {
private Object instance;
private static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
//objA 中有objB,objB中有objA
objA.instance = objB;
objB.instance = objA;
//虽然objA 和objB 置空,但这是指将他们的引用置空,在堆内存中,这两个对象还是互相持有\依赖的,这就是循环引用。
objA = null;
objB = null;
}
public static void main(String[] args) {
testGC();
}
}
现在,在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,即可被回收,又称为GC Roots Tracing算法。如下图所示,对象object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
为了增加垃圾收集的灵活性。实际上,一个到GC Roots没有任何引用链相连的对象有可能在某一个条件下“ 复活” 自己。对象的状态可以简单分成三类:
即使在一次可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
关于两次标记的Java代码源码,可以看这个文章:Java中的Finalizer类以及GC二次标记过程中的Java源码解析。
案例演示:
/**
1. 此代码演示了两点:
2. 1.对象可以在被GC时自我拯救。
3. 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
4. 5. @author zzm
*/
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK = null;
private static void isAlive() {
if (SAVE_HOOK != null) {
System.out.println("yes,i am still alive:)");
} else {
System.out.println("no,i am dead :(");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
//持有引用,对象"复活"
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
/*测试对象第一次成功拯救自己*/
//首先引用置null,该对象没有其他引用.
SAVE_HOOK = null;
//然后尝试进行一次gc,此时会执行回收SAVE_HOOK类,但是会执行finalize方法,在finalize中对其进行复活
System.gc();
//因为finalize方法由低优先级的线程finalizer调用,优先级很低,所以暂停0.5秒以等待它执行
Thread.sleep(500);
//测试是否复活
isAlive();
/*下面这段代码与上面的完全相同,但是这次自救却失败了。*/
SAVE_HOOK = null;
//然后再次尝试进行一次gc,此时会执行回收SAVE_HOOK类,但是不会执行finalize方法,因为fiinalize已经执行过一次了,第二次不会执行,这次自救却失败了
System.gc();
//因为finalize方法由低优先级的线程finalizer调用,优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
isAlive();
}
}
代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
Java中只有构造函数并没有析构函数一说,这里finalize()方法看起来像是实现了析构函数,但这只是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协。
现在,finalize()能做的所有工作,例如关闭外部资源等,使用try-finally或者其他方式都可以做得更好、更及时,因此finalize()方法不建议被使用。
析构函数(destructor): 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。
但是判断一个类是否是“无用的类”却需要同时满足下面3个条件才能行:
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
在大量使用反射、动态代理, CGLib等Byt式ode框架、动态生成JSP 以及OSG,这类频繁(自定义ClassLoader的场景都需耍虔拟机具备类卸载的功能,以保证永久代不会溢出。
相关文章:
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!