JVM判断对象是否存活

896 阅读8分钟

堆中几乎存放着所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”,那些对象已经“死去”(即不可能被任何途径使用的对象)

一、引用计数算法(Reference Counting)

给对象中添加一个引用计数器,每当又一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能在被使用的.但是Java语言中没有选用引用计数法来管理内存,其中最主要的原因是它很难解决对象之间相互循环依赖的问题.

例如: 在testGC中,对象objA和对象objB都有字段instance,赋值objA.instance=objB.instance及objB.instance=objA.instance,除此之外这两个对象再无其他任何引用,实际上这两个对象都已经不能再被访问,但是他们因为他们互相引用着对方,他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们. 打印GC详细信息: -XX:+PrintGCDetails

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;


        objA = null;
        objB = null;

        //假设在这行发生了gc,objA和objB是否被回收
        System.gc();
    }
}

GC日志为:

[GC (System.gc()) [PSYoungGen: 334480K->4736K(334848K)] 597914K->270331K(1017536K), 0.0019879 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 2408K->0K(298688K)] [ParOldGen: 0K->3363K(682688K)] 3408K->3363K(981376K), [Metaspace: 3162K->3162K(1056768K)], 0.0050515 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 153088K, used 3947K [0x0000000715580000, 0x0000000720000000, 0x00000007c0000000)
  eden space 131584K, 3% used [0x0000000715580000,0x000000071595afc0,0x000000071d600000)
  from space 21504K, 0% used [0x000000071d600000,0x000000071d600000,0x000000071eb00000)
  to   space 21504K, 0% used [0x000000071eb00000,0x000000071eb00000,0x0000000720000000)
 ParOldGen       total 349696K, used 872K [0x00000005c0000000, 0x00000005d5580000, 0x0000000715580000)
  object space 349696K, 0% used [0x00000005c0000000,0x00000005c00da3b8,0x00000005d5580000)
 Metaspace       used 3169K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 346K, capacity 388K, committed 512K, reserved 1048576K

GC日志分析:

GC (minor )日志:

Full GC 日志:

后半段分析

对照上面的图,GC日志中的PSYoungGen(PS是指Parallel Scavenge)为Eden+FromSpace,而整个YoungGeneration为Eden+FromSpace+ToSpace。

我们设置的新生代大小为10240K,这包括9216K大小的PSYoungGen和1024K大小的ToSpace。其中,PSYoungGen中的Eden:FromSpace为8:1,

这包括8192K的Eden和1024K的FromSpace。

详细日志输出:

参数设置为:

-XX:+PrintGCDetails -XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=8 -XX:NewSize=10M -XX:MaxNewSize=10M

参数解释:

-XX:+PrintGCDetails 启用日志

-XX:-UseAdaptiveSizePolicy 禁用动态调整,使SurvivorRatio可以起作用

-XX:SurvivorRatio=8 设置Eden:Survivior=8

-XX:NewSize=10M -XX:MaxNewSize=10M 设置整个新生代的大小为10M

默认垃圾收集器为:Parallel Scavenge

在运行日志结果中我们可以看到GC日志包含“2408K->0k”,老年代从2408K(约2M,其实就是objA与objB)变为了512K,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也证明虚拟机并不是通过引用计数法判断对象是否存活的.可以看到对象进入了老年代,但是大家知道,对象刚创建的时候是分配到新生代中到,要进入老年代需要到new ObjA才行,但是这里objA和objB却进入了老年代.这是因为Java堆区会动态增长,刚开始时堆区较小,对象进入老年代还有一些规则,当survior空间中同一代的对象大小之和超过survior空间的一半时,对象将直接进入老年代.

二、可达性分析算法(GC Roots Analysis):主流用这个判断

这个算法的基本思路是通过一系列名为“GC Root”的对象作为起点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用到,下图对象object5,object6,object7虽然有互相判断,但它们到GC Roots是不可达到,所以它们将会判定为可回收对象

在Java语言里,可作为GC Roots对象的包括如下几种:

  • 1、虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 2、方法区中的类静态属性引用的对象
  • 3、方法区中的常量引用的对象
  • 4、本地方法栈中JNI的引用的对象

三、finalize()方法最终判定对象是否存活

即使在可达分析算法中不可达的对象,也并非“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程. 标记的前提是对象在进行可达性分析后发现没有与GC Roots互相连的引用链.

1、第一次标记并进行一次筛选

筛选的条件是此对象是否有必要执行finalize()方法.当对象没有覆盖finalize()方法.或者finalize方法已经被虚机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收

2、第二次标记

如果这个对象被判定为有必要执行finalize()方法,那么对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它.这里的所谓的“执行”是指虚拟机会触发这个方法,但并不会承诺会等待它运行结束,这样做的原因,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃.finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己---只要重新与引用链上的任何一个对象建立关联既可.譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么第二次标记时它将被移除“即回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了.

流程图如下:

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("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功解救自己
        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:(");
        }

        //下面这段代码与上面的完全的相同,但是这次自救却失败了
        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:)");
        }
    }
}

运行结果:

finalize method executed!
yes,i am still alive:
no,i am dead:)

从运行结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了. 代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行为失败了.finzlize()能做的所有工作,使用try-finally或者其他方式都可以做更好、更及时.但是建议大家完全可以忘掉java语言有这个方法的存在.