专栏原创出处:github-源笔记文件 (opens new window)github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。

Java JVM-虚拟机专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java JVM-虚拟机 (opens new window)

# 一、引用的概念

JDK 1.2 版之后引入了软(SoftReference)、弱(WeakReference)、虚(PhantomReference)三种引用。

  • 强引用:最传统的「引用」的定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj=new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

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

  • 弱引用:描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

# 二、引用到底有什么作用

假设我们有一个对象 Data ,还有一个对象 Entry 中依赖 Data 对象。伪代码如下:

class Data {
    byte[] v;
}

class Entry {
    Data d;
}

Data data = new Data(new byte[10 * 1024]);

Entry entry = new Entry(data);

如果在运行过程中,data = null 后,data 对象可以被垃圾回收掉吗?

答案是:需要看 entry 对象是否为 null

如果 entry 一直不为 null 的话,data 永远不能被回收,因为 Entry.d 变量引用了 data。

这时就可能发生内存泄漏。

扩展知识:Java 是值传递还是引用传递?


那么如何解决呢,答案就是使用软、弱引用。

假如我们把 Entry 对 data 的依赖声明为一个软引用。如果 data = null 后,垃圾回收时就可以回收 data 对象了。

    class Entry extends WeakReference<Data> {

        public Entry(Data d) {
            super(d);
        }
    }

我们可以大白话的理解为:

  • 如果是弱引用,我对你的依赖很柔软薄弱,你觉得自己没有用了,我不会强行留住你,会放你走(垃圾回收)

  • 如果是强引用,就算你觉得自己没有用了,我依然不让你走(不让垃圾回收)

比喻的总结四个引用

  • 强引用:关系非常好,你自己没有用了,我也不会让你走

  • 软引用:关系还行,你自己没有用了,我会挽留到在系统将要发生内存溢出异常前在走

  • 弱引用:关系就那样,你自己没有用了,垃圾收集员一来你就可以走

  • 虚引用:关系近乎敌人,我永远得不到你,垃圾收集员一来你就可以走。主要与 ReferenceQueue 配合使用,在回收时进行一些逻辑操作(认为是回收前执行一个回调函数)

我们可以看到,最主要还是「你自己没有用了」这个操作,可以认为是一个 obj = null 的操作。如果你走了,那么我也拿不到你的信息了。

# 三、弱引用的 GC 实战

@Slf4j
public class WeakReferenceExample {

    public static void main(String[] args) throws InterruptedException {
        // 10M 的缓存数据
        byte[] cacheData = new byte[10 * 1024 * 1024];

        // 将缓存数据用软引用持有
        final WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);

        log.info("第一次 GC 前 {}", cacheData == null);
        log.info("第一次 GC 前 {}", cacheRef.get() == null);

        // 进行一次 GC 后查看对象的回收情况
        System.gc();
        Thread.sleep(1000); // 等待 GC
        log.info("第一次 GC 后 {}", cacheData == null);
        log.info("第一次 GC 后 {}", cacheRef.get() == null);

        // 将缓存数据的强引用去除,你没有用了
        cacheData = null;
        System.gc();
        Thread.sleep(1000); //等待 GC
        log.info("第二次 GC 后 {}", cacheData == null);
        log.info("第二次 GC 后 {}", cacheRef.get() == null);
    }
    /* 打印内容如下:
    
     第一次 GC 前 false
     第一次 GC 前 false
    
    [GC (System.gc())  14908K->11560K(125952K), 0.0318128 secs]
    [Full GC (System.gc())  11560K->11425K(125952K), 0.0216147 secs]
    
     第一次 GC 后 false
     第一次 GC 后 false
    
    [GC (System.gc())  12090K->11457K(125952K), 0.0016023 secs]
    [Full GC (System.gc())  11457K->818K(125952K), 0.0093186 secs]
    
     第二次 GC 后 true
     第二次 GC 后 true
     */
}

# 四、再理解 ThreadLocalMap 的弱引用

   static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

如果我们的代码中不需要 ThreadLocal 这个对象的话,即 ThreadLocal = null。但是 ThreadLocalMap 是线程的变量,如果线程一直运行,那么 ThreadLocalMap 永远不会为 null。

  • 如果使用强引用,Entry 中的 k 强引用了 ThreadLocal ,ThreadLocal 永远不能释放

  • 如果使用弱引用,ThreadLocal 在垃圾回收时将释放,Entry 中的 k 将变为 null

# 五、ReferenceQueue 引用队列

引用队列,用于存放待回收的引用对象。

对于软引用、弱引用和虚引用,如果我们希望当一个对象被垃圾回收器回收时能得到通知,进行额外的处理,这时候就需要使用到引用队列了。

在一个对象被垃圾回收器扫描到将要进行回收时 reference 对象会被放入其注册的引用队列中。我们可从引用队列中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理,资源释放等。


下面使用「虚引用」与「引用队列」实战说明 源码 (opens new window)

  • 创建一个 Map ,Key 是一个虚引用,虚引用关联 ReferenceQueue 队列,每当 Key 被回收时,这个 Key 会入队列。

  • 起一个线程不停的取队列中的回收对象进行打印操作。

  • 向 Map 循环 N 次,每次 put 一个大小为 1M 的字节数组,随着内存增长,垃圾回收器开始工作。

  • 垃圾回收器工作时,可以看到队列中将被回收的对象信息。

@Slf4j
public class PhantomReferenceExample {

    private static final ReferenceQueue<byte[]> RQ = new ReferenceQueue<>();

    public static void main(String[] args) {
        final Map<PhantomReference<byte[]>, Object> map = new HashMap<>();

        final Thread thread = new Thread(() -> {
            try {
                int cnt = 0;
                PhantomReference<byte[]> k;
                while ((k = (PhantomReference<byte[]>) RQ.remove()) != null) {
                    log.info("第 {} 个回收对象,对象打印为:{}", cnt++, k);
                }
            } catch (InterruptedException ignored) {
            }
        });
        thread.setDaemon(true);
        thread.start();

        for (int i = 0; i < 1000; i++) {
            map.put(new PhantomReference<>(new byte[1024 * 1024], RQ), new Object());
        }

        log.info("map.size :{}", map.size());
    }
    /* 部分输出如下:
     * 第 789 个回收对象,对象打印为:java.lang.ref.PhantomReference@26653222
     * 第 790 个回收对象,对象打印为:java.lang.ref.PhantomReference@553f17c
     * 第 791 个回收对象,对象打印为:java.lang.ref.PhantomReference@56ac3a89
     * 第 792 个回收对象,对象打印为:java.lang.ref.PhantomReference@6fd02e5
     * 第 793 个回收对象,对象打印为:java.lang.ref.PhantomReference@2b98378d
     * 第 794 个回收对象,对象打印为:java.lang.ref.PhantomReference@26be92ad
     * 第 795 个回收对象,对象打印为:java.lang.ref.PhantomReference@6d00a15d
     * map.size :1000
     */
}

一般情况我们很少使用软、弱、虚三种引用,如果使用请深入研究其利害,避免引起不必要的 Bug ,通常情况多用于缓存操作,防止缓存无限增长导致内存溢出。

# 六、应用场景

  • WeakHashMap 实现类,如果 WeakHashMap 中的 Key 对象如果不需要了,WeakHashMap 内部可以配合 ReferenceQueue 引用队列进行移除

  • 缓存的实现,因为缓存一般情况会长时间存活,如果缓存的元素已经失效了,使用软弱引用配合 ReferenceQueue 引用队列可以执行清除操作

  • 使用虚引用,完成垃圾回收时的消息回调等操作

# 总结

  • 引用可区分为强、软、弱、虚四种,后三种可配合「引用队列」进行一些回收前的操作

# 参考

最后修改时间: 2/17/2020, 4:43:04 AM