概念及应用场景

  • 强引用:Java中的引用,默认都是强引用。比如new一个对象,对它的引用就是强引用。对于被强引用指向的对象,就算JVM内存不足OOM,也不会去回收它们。
  • 软引用:若一个对象只被软引用所引用,那么它将在JVM内存不足的时候被回收,即如果JVM内存足够,则软引用所指向的对象不会被垃圾回收(其实这个说法也不够准确,具体原因后面再说)。根据这个性质,软引用很适合做内存缓存:既能提高查询效率,也不会造成内存泄漏。
  • 弱引用:若一个对象只被弱引用所引用,那么它将在下一次GC中被回收掉。如ThreadLocal和WeakHashMap中都使用了弱引用,防止内存泄漏。
  • 虚引用:虚引用是四种引用中最弱的一种引用。我们永远无法从虚引用中拿到对象,被虚引用引用的对象就跟不存在一样。虚引用一般用来跟踪垃圾回收情况,或者可以完成垃圾收集器之外的一些定制化操作。Java NIO中的堆外内存(DirectByteBuffer)因为不受GC的管理,这些内存的清理就是通过虚引用来完成的。
  • 终结器引用:无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

引用队列

引用队列(Reference Queue)是一个链表,顾名思义,存放的是引用对象(Reference对象)的队列。
软引用与弱引用可以和一个引用队列(Reference Queue)配合使用,当引用所指向的对象被垃圾回收之后,该引用对象本身会被添加到与之关联的引用队列中,从而方便后续一些跟踪或者额外的清理操作。
因为无法从虚引用中拿到目标对象,虚引用必须和一个引用队列(Reference Queue)配合使用。

简单测试

package com.unclezs.samples.java.reference;

import lombok.extern.slf4j.Slf4j;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Arrays;

/**
 * 几种引用类型测试
 * <p>
 * 指定VM参数 -Xms20m -Xmx20m -XX:+PrintGCDetails -verbose:gc
 *
 * @author zhanghongguo@sensorsdata.cn
 * @since 2020/12/17 13:56
 */
@Slf4j
public class ReferenceSamples {
  private static final int SIZE1MB = 1024 * 1024;
  private static final int SIZE1KB = 1024;

  public static void main(String[] args) throws InterruptedException {
    // 引用队列,存放Reference对象
    ReferenceQueue<Byte[]> queue = new ReferenceQueue<>();
    // 定义四种引用对象,强/弱/虚引用为1kb,软引用为1mb
    Byte[] strong = new Byte[SIZE1KB];
    SoftReference<Byte[]> soft = new SoftReference<>(new Byte[SIZE1MB * 10], queue);
    WeakReference<Byte[]> weak = new WeakReference<>(new Byte[SIZE1KB], queue);
    PhantomReference<Byte[]> phantom = new PhantomReference<>(new Byte[SIZE1KB], queue);

    Reference<? extends Byte[]> collectedReference;
    // 初始状态
    log.info("初始值 强引用: {}", Arrays.hashCode(strong));
    log.info("初始值 软引用: {}", Arrays.hashCode(soft.get()));
    log.info("初始值 弱引用: {}", Arrays.hashCode(weak.get()));
    log.info("初始值 虚引用: {}", Arrays.hashCode(phantom.get()));
    do {
      collectedReference = queue.poll();
      log.info("初始值 引用队列: " + collectedReference);
    } while (collectedReference != null);
    log.info("********************");
    // 第一次手动触发GC
    System.gc();
    // 停100ms保证垃圾回收已经执行
    Thread.sleep(100);

    log.info("GC后 强引用: {}", Arrays.hashCode(strong));
    log.info("GC后 软引用: {}", Arrays.hashCode(soft.get()));
    log.info("GC后 弱引用: {}", Arrays.hashCode(weak.get()));
    log.info("GC后 虚引用: {}", Arrays.hashCode(phantom.get()));
    do {
      collectedReference = queue.poll();
      log.info("GC后 引用队列: " + collectedReference);
    }
    while (collectedReference != null);
    log.info("********************");

    // 再分配1M的内存,以模拟OOM的情况
    Byte[] newByte = new Byte[SIZE1MB * 15];

    log.info("Full GC后 强引用: {}", Arrays.hashCode(strong));
    log.info("Full GC后 软引用: {}", Arrays.hashCode(soft.get()));
    log.info("Full GC后 弱引用: {}", Arrays.hashCode(weak.get()));
    log.info("Full GC失败后 虚引用: {}", Arrays.hashCode(phantom.get()));
    do {
      collectedReference = queue.poll();
      log.info("Full GC失败后 引用队列: " + collectedReference);
    }
    while (collectedReference != null);
  }
}

注意设置VM参数,每个机器不一样,你可以试着调试到合适自己的。

  • 初始状态下,虚引用用就返回null,其他三个引用都有值。
  • 当触发GC之后,弱引用指向的对象也被回收了,而且可以看到弱引用和虚引用两个引用对象被加到了它们相关联的引用队列中了;强引用和软引用还是可以取到值。
  • 当JVM内存不足之后,软引用也被内存回收了(可以看到软引用,第一次gc后还是内存不足,第二次gc时候,回收了软引用对象),同时该软引用也被加到了与之关联的引用队列中了。而强引用依然能取到值。

源码解析

Reference类

弱引用,软引用和虚引用都继承自Reference类,我们从Reference类看起

// 此Reference对象可能会有四种状态:active, pending, enqueued, inactive
// avtive: 新创建的对象状态是active
// pending: 当Reference所指向的对象不可达,并且Reference与一个引用队列关联,那么垃圾收集器
//     会将Reference标记为pending,并且会将之加到pending队列里面
// enqueued: 当Reference从pending队列中,移到引用队列中之后,就是enqueued状态
// inactive: 如果Reference所指向的对象不可达,并且Reference没有与引用队列关联,Reference
//     从引用队列移除之后,变为inactive状态。inactive就是最终状态
public abstract class Reference<T> {
    // 该对象就是Reference所指向的对象,垃圾收集器会对此对象做特殊处理。
    private T referent;         /* Treated specially by GC */
    // Reference相关联的引用队列
    volatile ReferenceQueue<? super T> queue;
    // 当Reference是active时,next为null
    // 当该Reference处于引用队列中时,next指向队列中的下一个Reference
    // 其他情况next指向this,即自己
    // 垃圾收集器只需判断next是不是为null,来看是否需要对此Reference做特殊处理
    volatile Reference next;
    // 当Reference在pending队列中时,该值指向下一个队列中Reference对象
    // 另外垃圾收集器在GC过程中,也会用此对象做标记
    transient private Reference<T> discovered;  /* used by VM */

    // 锁对象
    static private class Lock { }
    private static Lock lock = new Lock();

    // pending队列,这里的pending是pending链表的队首元素,一般与上面的discovered变量一起使用
    private static Reference<Object> pending = null;
    // 获取Reference指向的对象。默认返回referent对象
    public T get() {
        return this.referent;
    }
}

Reference类跟垃圾收集器紧密关联,其状态变化如下图所示:

上述步骤大多数都是由GC线程来完成,其中Pending到Enqueued是用户线程来做的。Reference类中定义了一个子类ReferenceHandler,专门用来处理Pending状态的Reference。我们来看看它具体做了什么。

ReferenceHandler类

public abstract class Reference<T> {
    // 静态块,主要逻辑是启动ReferenceHandler线程
    static {
        // 创建ReferenceHandler线程
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());
            Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 设置成守护线程,最高优先级,并启动
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        // 访问控制
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

    // 内部类ReferenceHandler,用来处理Pending状态的Reference
    private static class ReferenceHandler extends Thread {
        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }
        // 静态块,确保InterruptedException和Cleaner已经被ClassLoader加载
        // 因为后面会用到这两个类
        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            // 死循环调用tryHandlePending方法
            while (true) {
                tryHandlePending(true);
            }
        }
    }
}

Reference类在加载进JVM的时候,会启动ReferenceHandler线程,并将它设成最高优先级的守护线程,不断循环调用tryHandlePending方法。
接下来看tryHandlePending方法:

// waitForNotify默认是true。
static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        // 需要在同步块中进行
        synchronized (lock) {
            // 判断pending队列是否为空,pending是队首元素
            if (pending != null) {
                // 取到pending队列队首元素,赋值给r
                r = pending;
                // Cleaner类是Java NIO中专门用来清理堆外内存(DirectByteBufer)的类,这里对它做了特殊处理
                // 当没有其他引用指向堆外内存时,与之关联的Cleaner会被加到pending队列中
                // 如果该Reference是Cleaner实例,那么取到该Cleaner,后续可以做一些清理操作。
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // r.discovered就是下一个元素
                // 以下操作即为将队首元素从pending队列移除
                pending = r.discovered;
                r.discovered = null;
            } else {
                // 如果pending队列为空,则释放锁等待
                // 当有Reference添加到pending队列中时,ReferenceHandler线程会从此处被唤醒
                if (waitForNotify) {
                    lock.wait();
                }
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        // OOM时,让出cpu
        Thread.yield();
        return true;
    } catch (InterruptedException x) {
        return true;
    }
    // 给Cleaner的特殊处理,调用clean()方法,以释放与之关联的堆外内存
    if (c != null) {
        c.clean();
        return true;
    }
    // 此处,将此Reference加入到与之关联的引用队列
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

看到这里,豁然开朗。ReferenceHandler线程专门用来处理pending状态的Reference,跟GC线程组成类似生产者消费者的关系。当pending队列为空,则等待;当Reference关联的对象被回收,Reference被加入到pending队列中之后,ReferenceHandler线程会被唤醒来处理pending的Reference,主要做三件事:

  • 将该Reference从pending队列移除
  • 如果该Reference是Cleaner的实例,那么调用clean方法,释放堆外内存
  • 将Reference加入到与之关联的引用队列

软引用SoftReference

// 相比WeakReference,它增加了两个时间戳,clock和timestamp
// 这两个参数是实现他们内存回收上区别的关键
public class SoftReference<T> extends Reference<T> {
    // 每次GC之后,若该引用指向的对象没有被回收,则垃圾收集器会将clock更新成当前时间
    static private long clock;
    // 每次调用get方法的时候,会更新该时间戳为clock值
    // 所以该值保存的是上一次(最近一次)GC的时间戳
    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }
    // 每次调用,更新timestamp的值,使之等于clock的值,即最近一次gc的时间
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

SoftReference除了多了两个时间戳之外,跟WeakReference几乎没有区别,它是如何做到在内存不足时被回收这件事的呢?其实这是垃圾收集器干的活。垃圾收集器回收SoftReference所指向的对象,会看两个维度:

  1. SoftReference.timestamp有多老(距上一次GC过了多久)
  2. JVM的堆空闲空间有多大

而具体什么时候回收SoftReference所指向的对象呢,可以参考如下公式:

interval <= free_heap * ms_per_mb

其中interval为上一次GC与当前时间的差值,以毫秒为单位;free_heap为当前JVM中剩余的堆空间大小,以MB为单位;ms_per_mb可以理解为一个常数,即每兆空闲空间可维持的SoftReference的对象生存的时长,默认为1000,可以通过JVM参数-XX:SoftRefLRUPolicyMSPerMB设置。
如果上述表达式返回false,则清理SoftReference所指向的对象,并将该SoftReference加入到pending队列中;否则不做处理。所以说在JVM内存不足的时候回收软引用这个说法不是非常准确,只是个经验说法,软引用的回收,还跟它存活的时间有关,甚至跟JVM参数设置(-XX:SoftRefLRUPolicyMSPerMB)都有关系!

弱引用(WeakReference)

// 更加简单,只重写了两个构造方法
public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

虚引用(PhantomReference)

一般用于获取被回收的时候的通知,比如NIO的直接内存属于JVM之外的,就用了这个实现的 具体可以看Cleaner类

// 灰常简单,只重写了一个构造方法,一个get方法
public class PhantomReference<T> extends Reference<T> {
    // get方法永远返回null
    public T get() {
        return null;
    }

    // 只提供了一个包含ReferenceQueue的构造方法,说明它必须和引用队列一起使用
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

终结器引用(FinalReference 与 Finalizer)

因为jvm只能管理jvm内存空间,但是对于应用运行时需要的其它native资源(jvm通过jni暴漏出来的功能):例如直接内存DirectByteBuffer,网络连接SocksSocketImpl,文件流FileInputStream等与操作系统有交互的资源,jvm就无能为力了,需要我们自己来调用释放这些资源方法来释放,为了避免对象死了之后,程序员忘记手动释放这些资源,导致这些对象有的外部资源泄露,java提供了finalizer机制通过重写对象的finalizer方法,在这个方法里面执行释放对象占用的外部资源的操作,这样使用这些资源的程序员即使忘记手动释放,jvm也可以在回收对象之前帮助释放掉这些外部资源,帮助我们调用这个方法回收资源的线程就是我们在导出jvm线程栈时看到的名为Finalizer的守护线程;

/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {

    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

相关链接

How Hotspot Clear Softreference
FinalReference类的功能
FinalReference和Finalizer的源码分析

评论

博客
分类
标签
归档
关于