在Java虚拟机中如何判断对象是否“存活”和“死去”?
下面就开始讲述我今天学到的两种算法:
1/1引用计数算法(Reference Counting):
概述:
给对象中添加一个引用计数器,每当有一个地方引用它时,引用计数器就加一,每当有一个地方失去引用的时候,引用计数器就减一,任何时候,引用计数器为0的对象就不可能再被使用。
优点:
实现简单,判定效率很高,大部分情况下是一个不错的算法,也有一些比较著名的案例:例如微软公司的COM(Component Object Model)技术等等。
缺点:
Java虚拟机中并没有选择使用这种算法来管理内存,原因就是它很难解决对象之间相互循环引用的问题。
举个栗子:
public class TestReference {
public Object object = null;
public static void main(String[] args) {
TestReference t1 = new TestReference();
TestReference t2 = new TestReference();
t1.object = t2;
t2.object = t1;
t1 = null;
t2 = null;
//假设在这里发生GC,那么t1和t2是否会被回收
System.gc();
}
}
上述代码中,实际上t1和t2对象已经不可能再被访问,但是他们都互相引用着对方,所以引用计数器都不为0,于是引用计数器算法就无法回收他们。
1/2可达性分析算法(Reachability Anaysis):
概述:
在主流的商用程序语言(Java,C#..)的主流实现中,都是称通过可达性分析来判断对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径叫做引用链(Reference Chain),当一个对象到GC Roots没有任何一个引用链相连时,就说明这个对象是不可用的,如图,虽然object5、6、7有所关联,但是没有到GC Roots的引用链,所以是不可以用的。这就很好的解决了对象的循环引用的问题。
可达性分析算法判断对象是否可以回收
在Java语言中,可以作为GC Roots的对象包括下面几种:
1、虚拟机栈(栈帧中的本地变量表)所引用的对象(正在使用的某个方法或者类)。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象(不是很懂.....,可能就指的是一般的常量吧)。
4、本地方法栈中JNI(即之前博客中所提到的Native方法)引用的对象。
1/3引用
概述:
在JDK1.2以前,Java中的引用定义为:如果reference类型的数据中储存的数值代表的另外一块内存的起始地址,就称这块reference内存代表着一个引用(即C语言中的指针)。
JDK1.2之后,针对那些“食之无味,弃之可惜”的对象Java对引用的概念进行了扩充,主要分为以下几种:
1、强引用:
表示代码中普遍存在的,例如 Student s = new Student(),只要强引用存在,垃圾回收器就永远不会回收被引用的对象。
2、软引用:
用来描述一些“有那么一点用,但不是很必须的对象”,当内存比较紧张时(比如即将发生内存溢出异常),会将这些对象列进回收的范围之中,进行二次回收。如果这次回收还没有空出足够的内存空间,才会抛出内存溢出异常。SoftReference类来实现软引用
3、弱引用:
与软引用差不多,不过它只能生存到下次垃圾回收之前。WeakReference类来实现。
4、虚引用:
也称为幽灵(幻影)引用。。。貌似还挺炫酷,它是最弱的一种引用关系,如果一个对象引用了虚引用的对象(例如 student = (虚引用),可能不太对,但应该差不多是这个意思),完全不会对该对象造成任何的影响,而且也无法通过虚引用来获取到一个对象的实例。PhantomReference类来实现。
1/4对象自救的机会
实际上,要真正的宣布一个对象死亡,至少要经过两次标记的过程。
第一次标记:
当一个对象在可达性分析后,被发现已经没有引用链了(上文提到的),那么它会被第一次标记,并且进行一个筛选,筛选的条件就是这个对象是否重写finalize()方法(我竟然不知道Object中竟然还有这么一个方法....)。当对象没有重写finalize()方法或者是finalize()方法已经被虚拟机调用过一次,那么这个对象就可以去死了。
第二次标记
如果对象在第一次标记后被判断能够执行finalize()方法,那么它会被放置在一个F-Queue队列中,并在稍后由一个虚拟机自己创建的低优先级线程Finalizer中去执行finalize()方法。但也不会老老实实的等finalize()方法运行完毕,就是说如果你的finalize()方法是一个死循环,人家这个线程不会让你一直执行下去的。
成功的拯救了自己
如果这个对象在finalize()方法中建立了有效的引用,那么它就可以复活。
一个栗子:
public class TestFinallize {
public static TestFinallize SAVE_ME = null;
public void isAlive() {
System.out.println("我还活着!!!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("我救了我自己!!!");
TestFinallize.SAVE_ME = this;
}
public static void main(String[] args) throws InterruptedException {
// 对象第一次拯救了自己
SAVE_ME = new TestFinallize();
SAVE_ME = null;
System.gc();
// 因为 finalize()优先级很低,所以要等待一会
Thread.sleep(500);
if (SAVE_ME != null) {
SAVE_ME.isAlive();
} else {
System.out.println("我已经死了");
}
// 第二次没能救回自己
SAVE_ME = null;
System.gc();
Thread.sleep(500);
if (SAVE_ME != null) {
SAVE_ME.isAlive();
} else {
System.out.println("我已经死了");
}
}
}
运行结果:
测试自救结果
总结:
从上面的代码中可以看出,第一执行垃圾回收时,finalize()方法确实实现了自救。但是第二次执行中没能自救成功,说明finalize()方法只能被执行一次。并且是低优先级的。不过话说回来,这个finalize()方法我直到看了这本书之前一直不知道它的存在。。。原来是因为这个方法是Java刚诞生时,为了使C/C++程序员更容易接受它所作出的一个妥协,它的代码运行代价高昂,不确定性大,无法保证每个对象的调用顺序,finalize()方法所能做的事情,使用try-finally或者其他方式可以更好更快的完成。所以该方法已经不建议使用0.0.....
1/5回收方法区
概述
其实方法区(HotSpot虚拟机中的永久代)也是有垃圾收集的,在JVM规范中确实说过可以不要求虚拟机在方法区实现垃圾收集。不过方法区的垃圾回收的性价比也确实很低。
永久代的垃圾收集主要有两部分内容
1、放弃的常量
举一个栗子:当一个常量“abc”在常量池中没有被任何变量引用比如String a =“abc”(这算有变量变量引用的) ,如果这个时候刚好发生垃圾回收的话,而且很必要的话(就是内存不太够),那么这个“abc”就会被提出常量池。
2、无用的类
判断“无用的类”的方法会比较严苛,必须同时满足以下三点:
1)该被的所有实例均已被回收。
2)该类的类加载器ClassLoader已被回收。
3)该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足以上3个条件的无用类进行回收,但并不是说必然回收。
在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGI这类频繁自定ClassLoader的场景都需要虚拟机具备类卸载(就是回收无用的类)的功能,以保证永久代不会溢出。
网友评论