JVM GC之对象生死

1.简述

在Java内存运行时区域的各个部分中,程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随着线程而亡。栈中的栈帧随着方法的进入和退出而有条不紊的进行着入栈和出栈操作。
每个栈帧需要分配多少内存基本上在类结构确定时就已知了,因此这几个区域的内存分配和回收具有可确定性,在这几个区域就不需要过多的考虑内存的分配和回收问题,因为方法结束或线程结束时,内存自然就释放了。而java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才能知道会创建哪些对象,这部分的内存分配和回收都是动态的,垃圾收集器所关注就是这部分内存,因此本文的后续讨论中的“内存”分配与回收也仅指这部分内存

2.如何判断对象死亡

在堆里存放着java几乎所有的对象实例,垃圾收集器在对堆中的内存进行回收时,第一件事情就是要确定哪些对象是“存活”的,哪些对象是“死亡”的。那么如何判断对象是否可回收呢,引用计数器算法和可达性分析法

2.1.引用计数算法

引用技术算法就是,给对象添加一个引用计数器,当对象被引用时计数器就加1;当引用失效时,计数器就减1;当计数器为0时,表示对象就不能再被使用了。

使用引用计数算法判断对象的存活,方法很简单,但是该方法很难解决对象之间循环引用的问题。

举个简单的例子:
对象objA和对象objB都有字段instance,对其赋值objA.instance=objB、objB.instance=objA,此时对象objA和objB互相引用着对方,导致他们的计数器都不为0,于是引用计数算法无法通知垃圾收集器回收他们

2.1.2.可达性分析法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。如图:对象object5,object6,object7虽然相互有关联,但是他们到GC Roots是不可达的,所以它们将会被判定为可回收对象
image

在Java语言中,可作为GC Roots的对象包括下面几种:
- [x] 虚拟机栈(栈帧中的本地变量表)中的引用对象
- [x] 方法区中类静态属性引用的对象
- [x] 方法区中常量引用的对象
- [x] 本地方法栈中JNI(Native方法)引用的对象

2.1.3.Java引用

无论是通过引用计数器算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象存活都与“引用”有关。

Java将引用分为强引用(Strong Reference)、软引用(Weak Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这四种引用强度一次逐渐减弱。

强引用(Strong Reference)

强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾收集器永远不会回收掉被引用的对象

Object obj=new Object() //强引用

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会随意回收具有强引用的对象来解决内存不足的问题。

如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
 

obj=null //告诉垃圾收集器此对象可以被回收

软引用(Weak Reference)

软引用是用来描述一些有用但非必须的对象。

String str=new String("123");                                     // 强引用  
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用    

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;

如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。

另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

弱引用(Weak Reference)

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示

public class WeakReferenceDemo {
    public static void main(String[] args) {
        WeakReference<User> reference=new WeakReference<User>(new User("make",19));
        System.out.println(reference.get());
        System.gc();//通知垃圾收集器回收资源
        System.out.println(reference.get());
    }

    @Data
    @AllArgsConstructor
    public static class User{
        private String name;
        private int age;
    }
}

输出

WeakReferenceDemo.User(name=make, age=19)
null

第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。

不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此),稍微修改一下代码,再看一下输出结果:

public class WeakReferenceDemo {
    public static void main(String[] args) {
        User user=new User("make",19);//强引用
        WeakReference<User> reference=new WeakReference<User>(user);
        System.out.println(reference.get());
        System.gc();//通知垃圾收集器回收资源
        System.out.println(reference.get());
    }

    @Data
    @AllArgsConstructor
    public static class User{
        private String name;
        private int age;
    }
}

输出

WeakReferenceDemo.User(name=make, age=19)
WeakReferenceDemo.User(name=make, age=19)

虚引用(Phantom Reference)

虚引用也称为幽灵引用或者幻影引用,他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对它的生命周期造成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,在Java中通过PhantomReference类来实现虚引用。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

引用总结

引用级别由高到低:强引用 > 软引用 > 弱引用 > 虚引用

引用类型 回收时机 用途 生存时间
强引用 从不 一般对象 jvm运行终止
软引用 内存不足时 对象缓存 内存不足时终止
弱引用 垃圾回收时 对象缓存 垃圾回收后终止
虚引用 未知 垃圾回收通知 垃圾回收后终止

3.对象的自我救赎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它处于待回收阶段,要真正的宣告一个对象的死亡,至少要经历两次标记过程:

第一次,可达性分析后发现对象不可达,将会被第一次标记并进行一次筛选,筛选条件是判断该对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或已经被虚拟机调用过finalize()方法,虚拟机将这两种情况称为“没有必要执行”

第二次,如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被放在一个叫做F-Queue的队列里,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记。如果对象在finalize()中成功拯救自己,重新与引用链上的任何一个对象建立了关联,那么第二次标记将把它移除“即将回收”的集合。否则,就会真正被回收

对象自我救赎实例:

package com.lkf.jvm;

/**
 * 此代码演示了两点:
 * 一是:对象可以被GC自我救赎
 * 二是:这种自我救赎的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 **/
public class FinalizeEscapeGCDemo {
    public static FinalizeEscapeGCDemo SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("Yes,I'm still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGCDemo.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGCDemo();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("No,I'm 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'm dead :)");
        }
    }
}

输出:

finalize method executed!
Yes,I'm still alive :)
No,I'm dead :)
posted @ 2018-05-12 22:46  IT码客  阅读(176)  评论(0编辑  收藏  举报