JVM 7-对象存活与引用

193 阅读8分钟

如何判断一个对象是否存活

1. 引用计数法

给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的。

不过这个方法有一种情况就是出现对象的循环引用时GC没法回收。

2. 可达性分析法

这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中。搜索当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。

如图所示,对象object 5object 6object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

主流的商用程序语言,例如Java,C#等都是靠这招去判定对象是否存活的。(了解一下即可)

在Java语言汇总能作为GC Roots的对象分为以下几种:

  • 虚拟机栈(栈帧中的本地方法表)中引用的对象(方法参数 和 方法内局部变量)
  • 方法区中静态变量所引用的对象(静态变量)
  • 方法区中常量引用的对象
  • 本地方法栈(即native修饰的方法)中JNI引用的对象(JNI是Java虚拟机调用对应的C函数的方式,通过JNI函数也可以创建新的Java对象。且JNI对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收)
  • 已启动的且未终止的Java线程

这种方法的优点是能够解决循环引用的问题,可它的实现需要耗费大量资源和时间,也需要GC(它的分析过程引用关系不能发生变化,所以需要停止所有进程)

如何宣告一个对象的真正死亡

首先必须要提到的是一个名叫 finalize() 的方法

finalize()Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用。

补充一句:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。

在Java9中已经被标记为 deprecated,且java.lang.ref.Cleaner(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比finalize来的更加的轻量及可靠。 判断一个对象的死亡至少需要两次标记

如果对象进行可达性分析之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。 GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。

引用

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

在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

软引用、弱引用对象的回收,都是建立在对象没有强引用的前提下的

定义一个被回收的对象GCTarget

public class GCTarget {
    /**
     * 对象的ID
      */
    public String id;

    /**
     * 占用内存空间
     */
    public byte[] buffer = new byte[1024];

    public GCTarget(String id) {
        this.id = id;
    }

    @Override
    protected void finalize() throws Throwable {
        // 执行垃圾回收时打印显示对象ID
        System.out.println("Finalizing GCTarget, id is : " + id);
    }
    
}

1. 强引用

强引用是使用最普遍的引用。如下,gcTarget就是强引用

GCTarget gcTarget = new GCTarget("hello");

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

测试:

public static void main(String[] args) {
    GCTarget gcTarget = new GCTarget("hello");

    // id为“hello”的GCTarget对象不可达,此时已经没有引用指向这个对象,这个对象将被回收
    gcTarget = null;
    
    // 执行垃圾回收
    System.gc();

    try {
        // 休息一会儿,等待上面的垃圾回收线程运行完成
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

打印结果:
Finalizing GCTarget, id is : hello

2. 软引用

软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

JVM在分配空间时,如果Heap空间不足,就会进行相应的GC,但是这次GC并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure),JVM会尝试第二次GC,回收软引用关联的对象。

伪代码:

if(JVM内存不足) {
    // 通知垃圾回收器进行回收
    System.gc();
    
    if(JVM内存仍然不足) {
        // 将软引用中的对象引用置为null
        str = null;
        // 通知垃圾回收器进行回收
        System.gc();
    }

}

测试:

// -Xmx200m -XX:+PrintGC
public static void main(String[] args) {
    //100M的缓存数据
    byte[] cacheData = new byte[100 * 1024 * 1024];
    //将缓存数据用软引用持有
    SoftReference<byte[]> cacheRef = new SoftReference<>(cacheData);
    //将缓存数据的强引用去除
    cacheData = null;
    System.out.println("第一次GC前" + cacheData);
    System.out.println("第一次GC前" + cacheRef.get());
    //进行一次GC后查看对象的回收情况
    System.gc();

    try {
        // 休息一会儿,等待上面的垃圾回收线程运行完成
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println("第一次GC后" + cacheData);
    System.out.println("第一次GC后" + cacheRef.get());

    //在分配一个120M的对象,看看缓存对象的回收情况
    byte[] newData = new byte[120 * 1024 * 1024];
    System.out.println("分配后" + cacheData);
    System.out.println("分配后" + cacheRef.get());
}
    
打印结果:
第一次GC前null
第一次GC前[B@74a14482
[GC (System.gc())  106537K->103184K(196608K), 0.0097282 secs]
[Full GC (System.gc())  103184K->103030K(196608K), 0.0061162 secs]
第一次GC后null
第一次GC后[B@74a14482
[GC (Allocation Failure)  104064K->103062K(196608K), 0.0016926 secs]
[GC (Allocation Failure)  103062K->103062K(196608K), 0.0016185 secs]
[Full GC (Allocation Failure)  103062K->103026K(196608K), 0.0069061 secs]
[GC (Allocation Failure)  103026K->103026K(196608K), 0.0021233 secs]
[Full GC (Allocation Failure)  103026K->607K(150016K), 0.0064739 secs]
分配后null
分配后null

应用:

像这种如果内存充足,GC时就保留,内存不够,GC再来收集的功能很适合用在缓存的引用场景中。 在使用缓存时有一个原则,如果缓存中有就从缓存获取,如果没有就从数据库中获取,缓存的存在是为了加快计算速度, 如果因为缓存导致了内存不足进而整个程序崩溃,那就得不偿失了。

3. 弱引用

弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。

测试:

// -Xmx200m -XX:+PrintGC
public static void main(String[] args) {
    //100M的缓存数据
    byte[] cacheData = new byte[100 * 1024 * 1024];
    //将缓存数据用软引用持有
    WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
    System.out.println("第一次GC前" + cacheData);
    System.out.println("第一次GC前" + cacheRef.get());
    /*
    进行一次GC后查看对象的回收情况
    此时有强引用关联cacheData,对象不会被回收
     */
    System.gc();
    
    try {
        // 休息一会儿,等待上面的垃圾回收线程运行完成
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println("第一次GC后" + cacheData);
    System.out.println("第一次GC后" + cacheRef.get());

    //将缓存数据的强引用去除
    cacheData = null;
    System.gc();
    
    try {
        // 休息一会儿,等待上面的垃圾回收线程运行完成
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println("第二次GC后" + cacheData);
    System.out.println("第二次GC后" + cacheRef.get());
}

打印结果:
第一次GC前[B@74a14482
第一次GC前[B@74a14482
[GC (System.gc())  106537K->103152K(196608K), 0.0022720 secs]
[Full GC (System.gc())  103152K->103030K(196608K), 0.0051191 secs]
第一次GC后[B@74a14482
第一次GC后[B@74a14482
[GC (System.gc())  104064K->103062K(196608K), 0.0017224 secs]
[Full GC (System.gc())  103062K->626K(196608K), 0.0041642 secs]
第二次GC后null
第二次GC后null

应用:

WeakHashMap

4. 虚引用

public class PhantomReference<T> extends Reference<T> {  
...  
    public T get() 
        return null;  
    }  
} 
  • 虚引用形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,get方法只返回null
  • 如果一个对象仅持有虚引用,那么它就和没有任何引用一样(不可达),在任何时候都可能被垃圾回收器回收
  • 唯一的作用就是可以用来 记录对象是什么时候被GC回收的(即跟踪对象被垃圾回收器回收的活动)
  • 引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中

测试:

public static void main(String[] args) {
    GCTarget gcTarget = new GCTarget("hello");

    ReferenceQueue<GCTarget> queue = new ReferenceQueue<>();
    // 创建虚引用,要求必须与一个引用队列关联
    PhantomReference<GCTarget> reference = new PhantomReference<>(gcTarget, queue);

    gcTarget = null;

    System.out.println("gcTarget : " + gcTarget);
    // get方法只返回null
    System.out.println("phantomReference : " + reference.get());

    // 调用垃圾回收
    System.gc();

    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

打印结果:
gcTarget : null
phantomReference : null
Finalizing GCTarget, id is : hello

应用:

apache.commons.io.FileCleaningTracker,将file和某个obejct绑定成一个tracker,该tracker直接extends PhantomReference,当你从ReferenceQueue中能够remove/poll到该trakcerobject)的时候,也就可以做清理与之关联的file了。(追踪器)

未完待续

  1. reference类详解
  2. finalize()方法与finalizer

参考