深入理解JVM(③)判断对象是否还健在?

前言

因为Java对象主要存放在Java堆里,所以垃圾收集器(Garbage Collection)在对Java堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(不被引用了)。

判断对象是否健在的算法

1.引用计数算法

引用计数算法,很容易理解,在对象中添加一个引用计数器,每有一个地方引用它时,计数器值就加一;当引用失效是,计数器值就减一;任何时刻计数器为零的对象就是不可以能再被使用的对象
引用计数算法的原理简单,判定效率也很高。市面上也确实有一些技术使用的此类算法来判定对象是否存活,像ActionScript 3 的FlashPlayer、Python语言等。但是在主流的Java虚拟机里面都没有选用引用计算法来管理内存,主要是使用此算法时,必须要配合大量的额外处理才能保证正确的工作,例如要解决对象之间的相互循环引用的问题。

public class OneTest {

    public Object oneTest = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[256 * _1MB];


    /**
     * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过。
     */
    @Test
    public void testGC(){

        OneTest test1 = new OneTest();
        OneTest test2 = new OneTest();

        test1.oneTest = test2;
        test2.oneTest = test1;

        test1 = null;
        test2 = null;

        // 假设在这行发生GC,test1和test2是否能被回收?
        System.gc();

    }

}

分析代码,test1和test2对象都被设置成了null,在后面发生GC的时候,如果按照引用计数算法,这两个对象虽然都被设置成了null,但是test1引用了test2,test2又引用了test1,所以这两个对象的引用计数值都不为0,所以都不会被回收,但是真正的实际运行结果是,这两个对象都被回收了,这也说明HotSpot虚拟机并不是用引用计数法来进行的内存管理。

2. 可达性分析算法

当前主流的商用程序语言(Java、C#等),都是通过可达性分析(Reachability Analysis)算法来判断对象是否存活的。这个算法的基本思路就是通过一一系列称为“GC Roots” 的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索走过的的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots 间没有任何引用链相连,或者从GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
如下图,object10、object11、object12这三个对象,虽然互相有关联,但是它们到GC Roots是不可达的,因此它们会被判定为可回收的对象。
可达性分析算法
在Java程序中,固定可作为GC Roots 的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个现场被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
    除了这些固定的GC Roots集合以外,根据垃圾收集器以及当前回收的呢村区域不同,还会有其他对象“临时性”的加入,如果只针对Java堆中某一块儿区域发起垃圾收集时(例如只针对年轻代的垃圾收集),必须考虑到当前区域内的对象是否有被其他区域的对象所引用,这个时候就需要把这些关联区域的对象一并加入GC Roots集合中,来保证可达性分析的正确性。

重申引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判断对象是否存活都和“引用”离不开关系。在JDK1.2之前,Java里对引用的概念是:如果reference类型的数据中存储的数值代表的是另外一块儿内存的地址,就称该reference数据是代表某块内存、某个对象的引用。
在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用是最传统的“引用”的定义,指引用复制,即类似
Object obj = new Object()

这种引用关系。无论在任何情况下,只要强引用关系还存在,垃圾收集器就不会回收掉被引用的对象。

  • 软引用是用来描述一些还有用,但非必须的对象。在系统发生内存溢出前,会先对软引用对象进行第二次回收,如果回收后还没有足够的内存,才会抛出内存溢出的异常。
  • 弱引用也是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,弱引用的对象,只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用也称为“幽灵引用”或“幻影引用”,它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

判断对象是生是死的过程

即使在可达性分析算法中,判断为不可达的对象,也不是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记过程:

  • 如果第一次对象在进行可达性分析后发现与GC Roots 不可达,将进行第一次标记。
  • 随后对此对象进行一次是否有必要执行finalize()方法进行筛选,假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,都视为“没有必要执行”。
    如果对象被判定有必要执行finalize()方法,会将对象放置在一个名为F-Queue的队列中,并在由一条由虚拟机自动建立的、低调度的线程区执行它们的finalize()方法。但并不承诺一定会等待它们运行结束。

需要注意的是:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临第二次回收,它的finalize()方法不会被再次执行。
还有一点就是Java官方已经明确声明不推荐手动调用finalize()方法了,因为它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,并且finanlize()能做的所有工作,使用try-finally或其他方式都可以做的更好、更及时。

回收方法区

方法区垃圾收集的“性价比”通常比较低,并且方法区回收也有过于苛刻的判定条件。
方法区的垃圾收集主要回收两部分内容:废弃的常量不再使用的类型,回收废弃常量时,如果当前系统没有一个常量的值是当前常量值,且虚拟机中也没有其他地方引用这个常量。如果这个时候发生垃圾回收,常量就会被系统清理出常量池。
判定一个类型是否属于“不再使用的类”的条件就比较苛刻了,要同时满足如下三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的冲加载等,否则通常很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

同时满足了上述的三个条件后,也只是被允许进行回收了,关于是否要对类型进行回收还要对虚拟机进行一系列的参数设置,这里就不赘述了,感兴趣的可以自己去查询。

Tags: