End

强引用 软引用 弱引用 虚引用 引用队列

本文地址


目录

Java 中的引用

强引用 软引用 弱引用 虚引用

强引用

  • 当 JVM 内存空间不足时,JVM 宁愿抛出 OutOfMemoryError 使程序异常终止,也不会靠随意回收具有强引用的存活对象来解决内存不足的问题。

软引用

  • 只有当 JVM 认为内存不足时(即 JVM 会确保在抛出 OutOfMemoryError 之前),才会去试图回收软引用指向的对象。
  • 应用场景:用于内存没那么敏感的缓存,例如图片缓存、大文件缓存、数据库索引缓存。
  • 注意LruCache 中使用的是强引用,其控制缓存大小的方式是主动释放,而非被动等待 JVM 释放。

弱引用

  • 在 GC 线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
  • 由于 GC 是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。
  • 应用场景:用于内存非常敏感的缓存,还可以用于内存泄漏监测,例如 LeakCanay

虚引用

  • 虚引用也叫幻象引用,无法通过虚引用访问对象的任何属性或函数(因为 get 永远返回 null)。
  • 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被 GC 回收。
  • 幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。
  • 应用场景:可用来跟踪对象被 GC 回收的活动,当一个虚引用关联的对象被 GC 回收之前会收到一条系统通知。

WeakReference

Creates a new weak reference that refers to the given object.

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent); // 不关联任何 ReferenceQueue
    }

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

构造函数的作用为:

  • 通过 WeakReference 的构造方法创建了一个弱引用对象
  • 参数 referent 是被弱引用对象引用的对象,参数 q 是弱引用对象所关联的引用队列
  • 弱引用对象所引用的对象 referent 会在 gc 时被回收
  • 弱引用对象所引用的对象 referent 在被 gc 回收时,会通知到引用队列 q

ReferenceQueue

Reference 和引用队列 ReferenceQueue 联合使用时,如果 Reference 持有的对象被垃圾回收,JVM 就会把这个 Reference 加入到与之关联的引用队列中。

我们在创建各种引用并关联到相应对象时,如果指定了一个引用队列,JVM 会在特定时机将引用 enqueue(入队) 到引用队列里,我们可以通过调用引用队列的 remove 方法获取引用。

尤其是虚引用,由于其 get 方法永远返回 null,如果再不指定引用队列,其就没有什么意义了。我们可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

公共 API

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.

引用队列,在检测到适当的可达性更改后,垃圾收集器会将注册的引用对象附加到该队列中。

package java.lang.ref;

public class ReferenceQueue<T> {
    public ReferenceQueue()
    public Reference<? extends T> poll() // 轮询
    public Reference<? extends T> remove()
    public Reference<? extends T> remove(long timeout)
}

poll():轮询此队列以查看引用对象是否可用

Polls this queue to see if a reference object is available.

  • If one is available without further delay then it is removed from the queue and returned.
  • Otherwise this method immediately returns null.

remove():移除并返回此队列中的下一个引用对象

Removes the next reference object in this queue, blocking until either one becomes available or the given timeout period expires.

LeekCanary 的原理

使用 ReferenceQueue 可以用来检测内存泄露,大名鼎鼎的 LeekCanary 就是采用这种原理来检测的:

  • 监听 Activity 的生命周期
  • 在 onDestroy 的时候,创建相应的 Reference 和 ReferenceQueue,并启动后台进程去检测
  • 一段时间之后,从 ReferenceQueue 读取
    • 若读取不到相应 Activity 的 Reference,有可能发生泄露了,这个时候,再触发 gc
    • 一段时间之后再去读取,若从 ReferenceQueue 还是读取不到相应 Activity 的 Reference,基本可以断定是发生内存泄露了(不能 100% 保证,因为触发 gc 不一定真的 gc 了)
  • 检测到发生内存泄露之后,dump 内存快照,分析 hprof 文件,找到泄露路径

WeakHashMap

WeakHashMap 和 HashMap 继承体系基本一样(均继承自 AbstractMap、且实现 Map 接口),核心区别在于其内部的 Map.Entry

  • WeakHashMap 中的 Entry 继承自 WeakReference
    • 和 ThreadLocal.ThreadLocalMap 中的 Entry 一样,理解了 WeakHashMap也就理解了 ThreadLocalMap 的原理
  • HashMap 中的 Entry 就是一个普通的实体类

WeakHashMap.Entry

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    Entry(Object key, V value,  ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}

其中,super(key, queue) 的含义为:

  • 通过 WeakReference 的构造方法创建了一个弱引用
  • 弱引用所引用的对象为 key,对象 key 会在 gc 时被回收
  • 弱引用所引用的对象 key 在被 gc 时,会通知到引用队列 queue
    • 从而可以使被 gc 掉的 key 值所对应的 entry 可以从 map 中被移除
    • 从 map 中移除 entry 的时机是在调用 get、size、put 等方法时

简单来说就是,WeakHashMap 使用 WeakReference 当作 key 来进行数据的存储,当 key 中的引用被 gc 掉之后,它会自动的将相应的 entry 移除掉。

测试代码

Map<byte[], String> mMap = new WeakHashMap<>(); // 注意 key 不是 Reference,而是 Reference 引用的具体的对象
for (byte b = 0; b < Byte.MAX_VALUE; b++) {
    byte[] bytes = new byte[1 * 1024 * 1024]; // 可以通过调整大小来测试
    bytes[0] = b;
    mMap.put(bytes, "value = " + b);
}
System.out.println("mMap.size = " + mMap.size()); // 数量大大小于 Byte.MAX_VALUE,因为很多 Entry 被移除了

for (Map.Entry<byte[], String> entry : mMap.entrySet()) {
    if (entry != null && entry.getKey() != null) {
        byte[] bytes = entry.getKey();
        System.out.println("  存活对象:" + (bytes != null) + " - " + bytes[0] + " - " + mMap.get(bytes));
    }
}

杨晓峰:Java 核心技术面试精讲

4 种引用类型介绍

在 Java 语言中,除了原始数据类型的变量,其他都是引用类型。不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集(GC)的影响

  • 强引用,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应强引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

  • 软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

  • 弱引用(WeakReference),不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重新实例化。它同样是很多缓存实现的选择。

  • 虚引用(PhantomReference),你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,我在专栏上一讲中介绍的 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的创建和销毁。

对象可达性状态流转分析

下面的流程图简单总结了对象生命周期和不同可达性状态,以及不同状态可能的改变关系,可能未必 100% 严谨,来阐述下可达性的变化。

这是 Java 定义的不同可达性级别(reachability level),具体如下:

  • 强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。
  • 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
  • 弱可达(Weakly Reachable),就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。这是十分临近 finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。
  • 幻象可达(Phantom Reachable),就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
  • 不可达(unreachable),意味着对象可以被清除了。

判断对象可达性,是 JVM 垃圾收集器决定如何处理对象的一部分考虑。

所有引用类型,都是抽象类 java.lang.ref.Reference 的子类,它提供了 get() 方法:

public T get()
// Returns this reference object's referent. If this reference object has been cleared, either by the program or by the garbage collector, then this method returns null.

除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态!这也是为什么我在上面图里有些地方画了双向箭头

所以,对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以保证处于软引用、弱引用状态的对象,后面没有改变为强引用。

测试代码

综合案例

可以通过调整第 33 行的数组大小,以及第 35 行的 Reference 类型,观察不同情况下,对象的回收及存活情况。

public class Test {
    Map<Reference<byte[]>, String> mMap = new HashMap<>(); // 注意 key 是 Reference
    ReferenceQueue<byte[]> mQueue = new ReferenceQueue<>();

    public static void main(String[] args) {
        Test test = new Test();
        test.monitor(); // 监控 mMap 中 Reference 持有的对象何时被 GC 回收
        test.put(); // 向 mMap 中添加 Reference,Reference 持有的是超大的对象
        test.get(); // 获取 mMap 中存活的对象
    }

    private void monitor() {
        Thread thread = new Thread(() -> {
            try {
                int count = 0;
                Reference<? extends byte[]> reference;
                // 如果 Reference 持有的对象被 GC,JVM 就会把这个 Reference 加入到与之关联的 ReferenceQueue 中
                // 可以通过 remove 从 ReferenceQueue 获取此 Reference,remove 是一个阻塞方法,可以指定超时时间
                while ((reference = mQueue.remove()) != null) {
                    byte[] bytes = reference.get(); // 这里获取到的都是被回收的对象,所以这里的 bytes 都为 null
                    System.out.println("  回收对象:" + (++count) + " - " + (bytes == null) + " - " + mMap.get(reference));
                }
            } catch (InterruptedException e) {
                System.out.println("------------------ InterruptedException ------------------ ");
            }
        });
        thread.setDaemon(true);
        thread.start();
    }

    private void put() {
        for (byte b = 0; b < Byte.MAX_VALUE; b++) {
            byte[] bytes = new byte[10 * 1024 * 1024]; // 可以通过调整大小来测试
            bytes[0] = b;
            Reference<byte[]> reference = new WeakReference<>(bytes, mQueue); // 添加的是弱引用
            mMap.put(reference, "value = " + b);
        }
        System.out.println("mMap.size = " + mMap.size());
    }

    private void get() {
        int liveCount = 0, gcCount = 0;
        for (Map.Entry<Reference<byte[]>, String> entry : mMap.entrySet()) {
            if (entry != null && entry.getKey() != null) {
                Reference<? extends byte[]> reference = entry.getKey();
                if (reference != null) {
                    byte[] bytes = reference.get();
                    if (bytes != null) {
                        System.out.println("  存活对象:" + (++liveCount) + " - " + bytes[0] + " - " + mMap.get(reference));
                    } else {
                        gcCount++;
                    }
                } else {
                    // 注意回收的是 Reference 引用的对象,而非引用本身(引用本身是被 mMap 强应用的),所以这里正常不会发生
                    System.out.println("--------------- reference == null ---------------");
                }
            }
        }
        System.out.println("存活对象个数:" + liveCount);
        System.out.println("回收对象个数:" + gcCount);
    }
}

PhantomReference

ReferenceQueue<byte[]> mQueue = new ReferenceQueue<>();

byte[] bytes = new byte[1 * 1024 * 1024];
Reference<byte[]> reference = new PhantomReference<>(bytes, mQueue);
System.out.println(reference.get()); // 虚引用的 get 方法永远返回 null
System.out.println(mQueue.poll() == null); // true,此时还没加入引用队列中

// 被置为 null 后,当 GC 发现虚引用,GC 会将把 PhantomReference 对象加入到引用队列中
bytes = null;
System.gc();

new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(mQueue.poll() == null); // false,引用队列中的 Reference 一直存在
    }
}).start();

2016-04-25

posted @ 2016-04-25 14:37  白乾涛  阅读(12827)  评论(0编辑  收藏  举报