GC前,判断对象是否死亡

本文基于jdk 1.7,参考《深入了理解java虚拟机》一书

一、概述

垃圾自动回收机制是java语言相比c++的一大特性,但垃圾收集并不是java语言的伴生物,GC的历史比java更加久远。为什么我们要了解GC和内存分配呢,当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更大并发量的瓶颈时,我们就需要对垃圾自动回收机制进行调优。

二、判断对象是否需要回收

1、强软弱虚引用

判断对象是否存活与“引用有关”,在jdk 1.2之前,引用是reference类型的数据中存储的代表另一块内存的起始地址的数值。jdk 1.2之后,java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这四种引用强度依次减弱。

  • 强引用:在程序代码中普遍存在,类似"Object obj = new Object()",强引用的对象不会被垃圾收集器自动回收。
  • 软引用:描述一些还有用但非必需的对象。对于软引用对象,在系统将要发生内存溢出异常之前,将会把这些对象列如回收范围内进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。jdk提供SoftReference类实现弱引用。
  • 弱引用:描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,垃圾收集器工作时,无论这次回收还有没有足够的内存,都会回收只被弱引用关联的对象。jdk提供WeakReference类实现弱引用。
  • 虚引用:一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为对象设置虚引用关联的唯一目的是这个对象被垃圾收集器回收时得到一个系统通知。jdk提供PhantomReference类实现弱引用。

2、对象已死吗

java堆里存放着几乎所有的对象实例,垃圾收集器对堆进行回收前,第一件事就是判断那些对象还活着,哪些已死去(即不可能再被任何途径使用的对象)。
判断对象是否死亡理论上有两种方法:引用计数法、可达性分析。

(1)引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器值减1;任何时候计数器值为0的对象就是不可能在被使用的。
引用计数法优点是实现简单、判断效率高,但缺陷是没有解决对象循环引用的问题。比如两个对象相互引用,除此之外没有任何地方引用到这两个对象,这两个对象应当被回收,但是按照引用计数法它们的引用计数器值不为0,无法通知GC回收它们。
java虚拟机不采用引用计数法判断对象是否存活。

(2)可达性分析算法

可达性分析算法是主流java虚拟机判断对象是否存活采用的手段。这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索通过的路径称为引用链,当一个对象没有任何引用链与其相连时(也叫对象不可达),则证明此对象是不可用的。
在java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

3、起死回生:finalize()方法

要真正宣告一个对象死亡,至少需要经历两次标记过程。
通过可达性分析,不可达的对象将会被第一次标记并进行一次筛选,筛选的条件是判断对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者该对象的finalize方法已经被虚拟机调用过,虚拟机将这两种情况视为没有必要执行。(任何对象的finalize()方法都只会被系统调用一次!)
如果筛选结果是有必要执行finalize()方法,那么这个对象将被放置到一个叫做F-Queue的队列之中,并在稍后由虚拟机建立的低优先级的Finalizer线程去执行finalize()方法。
稍后GC将对F-Queue队列中的对象进行小规模标记,如果该对象在finalize()方法中重新与引用链上的任一对象关联上,则对象成功在finalize方法中拯救了自己,GC会在第二次标记过程中将此对象移出“即将回收”的集合。
代码示例:

public class TestFinalize {
    public static TestFinalize testFinalize = null;
    public void isAlive(){
        System.out.println("I am alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method excuted");
        testFinalize = this;
    }

    public static void main(String[] args) throws InterruptedException {
        testFinalize = new TestFinalize();

        testFinalize =null;
        System.gc();//此方法会调用finalize方法
        //因为执行finalize方法的线程优先级很低,停顿0.5秒等它执行
        Thread.sleep(500);
        if (testFinalize!=null){
            testFinalize.isAlive();
        }else{
            System.out.println("I am dead");
        }

        //与上面代码完全相同,但是这次自救失败了,原因是任何对象的finalize方法只会被执行一次
        testFinalize =null;
        System.gc();//此方法会调用finalize方法
        //因为执行finalize方法的线程优先级很低,停顿0.5秒等它执行
        Thread.sleep(500);
        if (testFinalize!=null){
            testFinalize.isAlive();
        }else{
            System.out.println("I am dead");
        }
    }
}

执行结果:
在这里插入图片描述

4、方法区回收

jdk1.8以前存在方法区,HotSpot虚拟机中也叫永久代,java的垃圾收集主要针对堆内存,但是永久代的内存也是可以发生垃圾回收的,永久代垃圾回收的效率远低于堆内存,HotSpot虚拟机提供了 -Xnoclassgc参数控制。
永久代的垃圾收集主要有两部分内容:回收废弃常量、类的卸载。
常量池中的废弃常量和类、方法、字段的符号引用没有在任何地方引用到,而且有必要的情况下,会被回收。
类的卸载要同时满足一下3个条件才会被回收:

  • 该类的所有实例都被回收
  • 加载该类的类加载器已被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足以上条件仅仅说明类可以被回收,要不要回收还需要参数控制以及看虚拟机是否支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章