Java之Reference体系浅析

148 阅读6分钟

本文中源码来自JDK 8

Java中有4种引用类型,帮助JVM更好地进行内存管理。深入理解Reference,是学习WeakHashMapThreadLocal、缓存组件等的基础。本文将带你全面学习Reference体系实现。

1 Java中的引用类型

在Java中,为了更好地进行内存管理,对一个类对象,可以创建它的多种引用类型:强引用、软引用、弱引用和虚引用。

// 强引用  
Object obj = new Object();  
// 软引用  
SoftReference<Object> softRef = new SoftReference<>(obj);  
// 弱引用  
WeakReference<Object> weakRef = new WeakReference<>(obj);  
// 虚引用  
ReferenceQueue<Object> queue = new ReferenceQueue<>();  
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);

1.1 强引用

Strong Reference,是Java中最为普遍的引用类型。当一个对象存在强引用时,垃圾回收器不会回收这个对象,即使系统内存不足。只有它的强引用被显式释放时,该对象才会成为垃圾回收的候选对象。

// 强引用  
Object obj = new Object();
// 释放强引用,可被垃圾回收
obj = null

1.2 软引用

Soft Reference,用于描述还有用但并非必须的对象,在内存不足时可以被垃圾回收。

例如,程序中的大对象,业务上的生命周期很长,又能够方便地重建该对象:

  • 如果仅使用它的强引用,将导致长期占用程序内存;
  • 如果使用软引用,允许内存不足时将它回收;
  • 当再次使用它时,已不能从软引用中获得,只需重建该对象即可。

可见,软引用将允许程序OOM前进行自救(回收软引用后可能会避免OOM);也是一种用时间(重建对象的耗时)换取空间(回收软引用的内存)的策略

某些程序缓存就可使用软引用。

  // 20MB 大对象
  public static SoftReference<byte[]> softRef = new SoftReference<>(new byte[20 * 1024 * 1024]);

  public static void softRef() {
    byte[] array = softRef.get();
    if (array == null) {
      // 已被回收,重建
      array = new byte[10 * 1024 * 1024];
      softRef = new SoftReference<>(array);
    }
    // 使用array,array的强引用将在当前方法退出时被释放
    doSomething(array);
  }

1.3 弱引用

Weak Reference,当一个对象只被弱引用关联时,即使内存并不紧张,在下一次垃圾回收时,该对象都就有可能被回收。

  • 如果在创建弱引用指定引用队列,弱引用对象被回收时,会把该对象放入引用队列中;
  • 从弱引用中获取对象,每次都要判断一下是否为空,来避免空指针异常
    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);
    // 手动触发gc
    System.gc();

    Object obj = weakRef.get();
    // 已被回收
    if (obj == null) {
      Reference<?> poll = queue.remove();
      System.out.println(poll);
      System.out.println(weakRef);
      System.out.println(poll == weakRef);
    }
    // 没被回收时,正常使用obj

image.png

1.4 虚引用

Phantom Reference,主要用于跟踪对象被垃圾回收的状态,无法通过该引用直接获取到对象实例。垃圾回收器随时可能回收只有被虚引用关联的对象。

ReferenceQueue<Object> queue = new ReferenceQueue<>();  
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);  
// 输出null  
System.out.println( phantomRef.get());  
System.gc();  
Reference<?> ref = queue.remove();  
// 输出 true  
System.out.println(ref == phantomRef);

2 Reference抽象类

强引用无需包装,其他引用类型都是Reference的子类。 image.png Reference关键属性有:

// 关联的对象
private T referent;

// 引用队列
volatile ReferenceQueue<? super T> queue;

// 在队列中的下一个引用,如果是最后一个引用,则nest指向自己
Reference next;

// 静态属性,充当入队操作的synchronized锁对象
private static Lock lock = new Lock();

// 静态属性,待入队引用
private static Reference<Object> pending = null;

// 成员属性,下一个待入队元素
transient private Reference<T> discovered;

2.1 待入队队列

对于static属性pending,源码中有如下解释:

等待进入队列的引用列表。垃圾收集器将引用添加到此列表中,而引用处理程序线程将它们删除。该列表受上述锁对象的保护。列表使用discovered属性来链接其中的元素。

可见,待入队引用,由JVM的垃圾回收器加入到pending队列中,队列内通过discovered相关联,pending指向头元素,先进先出。 image.png pending列表会被入队线程消费。

2.2 入队线程

Reference中嵌套类ReferenceHandler,是Thread的子类,在static块中创建了它的实例,最高优先级、守护模式,同时启动它。 image.png ReferenceHandler的run方法如下,主要逻辑有:

  1. pending为null时,入队线程将wait阻塞;
  2. 当JVM回收一个软引用、弱引用或虚引用(称为ref)时,将pending指向该ref,并调用notify()唤醒入队线程;
  3. 将pending指向ref.discovered,调用ReferenceQueue.enqueue()将ref入队;
  4. 进入下一次for循环。
public void run() {
    for (;;) {
        // 本次要入队的引用
        Reference<Object> r;
        // 防止并发
        synchronized (lock) {
            if (pending != null) {
                // 将pending入队
                r = pending;
                // 指向下一个待入队引用
                pending = r.discovered;
                // 
                r.discovered = null;
            } else {
                // 阻塞等待下一个要入队的引用
                try {
                    try {
                        lock.wait();
                    } catch (OutOfMemoryError x) { }
                } catch (InterruptedException x) { }
                continue;
            }
        }

        // 入队
        ReferenceQueue<Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
}

可见,ReferenceHandler就是一个消费者(生产者是JVM垃圾回收器),作用是将pending队列中引用异步放入ReferenceQueue

3 ReferenceQueue的作用

从Java1.2起,源码中介绍如下:

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

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

一个弱引用(或软引用、虚引用)何时被gc回收,不受用户线程控制。当一个弱引用(或软引用、虚引用)关联的对象被gc回收时,用户线程希望得到通知,进行一些额外处理,如数据清理等。目前有两种方式:

  • 可以通过weakRef.get()是否返回null来判断;
  • 使用ReferenceQueue,对象被gc回收时,JVM会将相应的引用放入ReferenceQueue中。

可见,ReferenceQueue是引用关联的对象被gc回收时,JVM给用户线程的通知队列。

3.1 主要属性

// 一个标识,代表不能入队
static ReferenceQueue<Object> NULL = new Null<>();
// 一个标识,代表已入队
static ReferenceQueue<Object> ENQUEUED = new Null<>();

// enqueue方法的synchronized锁对象
private Lock lock = new Lock();

// 队列的头元素
private volatile Reference<? extends T> head = null;

// 队列中元素个数
private long queueLength = 0;

NULL是ReferenceQueue的内部子类,enqueue()方法并不执行入队。 image.png 创建Reference时,如果不提供queue时,会将queue设置为NULL。 image.png

3.2 入队

enqueue()逻辑如下: image.png 引用已入队时,会将Reference.queue属性设置为ENQUEUED。 image.png

队列中,各元素通过Reference.next属性关联,形成单向链表,ReferenceQueue.head始终指向尾元素。 image.png

3.3 出队

出队有无阻塞的poll()、带阻塞的remove()。两者都会调用reallyPoll()方法:

  1. 将ReferenceQueue.head指向next节点,原head出队;
  2. 如果head为null,即队列中无引用,将返回null。

image.png image.png remove()不带超时,在队列为空时将一直阻,直到有引入队列时才被放行。

3.4 FILO特性

可见,ReferenceQueue不是先进先出的,而是先进后出的单向链表。

    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    WeakReference<Object> weakRef1 = new WeakReference<>(new Object(), queue);
    System.out.println("weakRef1: " + weakRef1);
    // 手动触发gc
    System.gc();

    TimeUnit.SECONDS.sleep(5);
    WeakReference<Object> weakRef2 = new WeakReference<>(new Object(), queue);
    System.out.println("weakRef2: " + weakRef2);
    System.gc();

    Reference<?> poll;
    while ((poll = queue.remove(2000)) != null) {
      System.out.println("dequeue:" + poll);
    }

执行上面代码,结果如下:weakRef2后入队,但是会先出队。 image.png