阅读 JDK 8 源码:WeakHashMap 和 Reference、ReferenceQueue

WeakHashMap 是一种特殊的 HashMap,它的 key 为 WeakReference 弱引用,并且内置了一个 ReferenceQueue 用于存储被回收的弱引用。阅读 WeakHashMap 源码之前,需要先理解 Reference 和 ReferenceQueue 的机制。理解其基本原理之后,可以使用 HashMap 达到跟 WeakHashMap 一样的效果,文末提供了示例。

1. Reference

public abstract class Reference
extends Object

Reference 是引用对象的抽象基类。此类定义了常用于所有引用对象的操作。因为引用对象是通过与垃圾回收器的密切合作来实现的,所以不能直接为此类创建子类。

Reference 继承结构

继承结构如下:

阅读 JDK 8 源码:WeakHashMap 和 Reference、ReferenceQueue_第1张图片

Reference 与 GC 的交互

在 Reference 实例所管理的对象被垃圾回收器检测为不可达时,垃圾回收器会把该 Reference 添加到 pending-Reference 列表中,这是一个非常轻量级的操作。同时,Reference 实例会默认启动一个 ReferenceHandler 守护线程,不停地尝试从 pending-Reference 列表中取得被回收的 Reference 实例。当获取成功时,ReferenceHandler 线程会把该 Reference 存入 ReferenceQueue 队列。

Reference 的构造函数

Reference 是一个抽象类,需要由子类来调用其构造方法。
入参 referent 是需要被 GC 特殊对待的对象 ,入参 queue 是 ReferenceQueue 引用队列实例,默认为 ReferenceQueue.NULL。

Reference(T referent) {
    this(referent, null);
}

Reference(T referent, ReferenceQueue queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

Reference 的属性

private T referent;  // Reference实例管理的对象,会被GC特殊对待

volatile ReferenceQueue queue; // Reference实例管理的对象被回收后,Reference实例会被添加到这个队列中

/* When active:   NULL
 *     pending:   this
 *    Enqueued:   队列中的下一个引用(如果是最后一个,则为this)
 *    Inactive:   this
 */
@SuppressWarnings("rawtypes")
Reference next; // ReferenceQueue队列的下一个引用

/* When active:   由垃圾回收器管理的已发现的引用列表的下一个元素(如果是最后一个,则为this)
 *     pending:   pending-Reference列表中的下一个元素(如果是最后一个,则为null)
 *   otherwise:   NULL
 */
transient private Reference discovered;  // pending-Reference列表的指针,由JVM使用

/* 用于控制垃圾回收器操作的锁,垃圾回收器开始一轮垃圾回收前要获取此锁。
 * 所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码。
 */
static private class Lock { }
private static Lock lock = new Lock();


/* pending-Reference列表,存储等待进入ReferenceQueue队列的引用。
 * 垃圾回收器向该列表添加元素,而Reference-handler线程向该列表移除元素。
 * 操作pending-Reference列表需要使用lock对象。
 * pending-Reference列表使用discovered指针来访问元素。
 */
private static Reference pending = null; // pending-Reference列表的头结点 
 

Reference 的状态

Reference 实例具有四种状态:

  • Active:新创建的 Reference 实例为 Active 状态。当垃圾回收器检测到 Reference 中管理的对象为不可达时,如果该 Reference 实例注册了队列,则进入 Pending 状态,否则进入 Inactive 状态。
  • Pending:在 pending-Reference 列表中的元素,等待 Reference-handler 线程将其存入 ReferenceQueue 队列。未注册的实例不会到达这个状态。
  • Enqueued:在 ReferenceQueue 队列中的元素。当实例从 ReferenceQueue 队列中删除时,进入 Inactive 状态。未注册的实例不会到达这个状态。
  • Inactive:一旦实例变为 Inactive (非活动)状态,它的状态将不再更改。

其状态图转换如下:

阅读 JDK 8 源码:WeakHashMap 和 Reference、ReferenceQueue_第2张图片

Reference 类是通过其中的 queue 属性和 next 属性来记录这些状态:

  • Active:queue = ReferenceQueue实例 或 ReferenceQueue.NULL; next = null
  • Pending:queue = ReferenceQueue实例; next = this
  • Enqueued:queue = ReferenceQueue.ENQUEUED; next = 队列的下一个节点或 this
  • Inactive:queue = ReferenceQueue.NULL; next = this.

ReferenceHandler 线程

Reference 类中定义了静态代码块,用于启动 ReferenceHandler 守护线程,设置优先级最高。

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    /* If there were a special system-only priority greater than
     * MAX_PRIORITY, it would be used here
     */
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start(); // 启动ReferenceHandler守护线程,优先级最高

    // provide access in SharedSecrets // 在SharedSecrets中提供访问权限
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

ReferenceHandler 线程启动之后,在死循环中执行 tryHandlePending 操作。
tryHandlePending 代码流程:

  1. 从 pending 属性取得 GC 回收时存入 pending-Reference 列表中对象。
  2. 将 pending 指向 pending-Reference 列表的下一个节点。
  3. 如果 ReferenceQueue 不为空,则把被回收的对象入队。

java.lang.ref.Reference.ReferenceHandler

private static class ReferenceHandler extends Thread {
    public void run() {
        while (true) {
            tryHandlePending(true);
        }
    }
}

java.lang.ref.Reference#tryHandlePending

static boolean tryHandlePending(boolean waitForNotify) {
    Reference r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending; // GC 回收的时候,会把对象赋值给 pending,这里取的该对象
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // 从pending链中删除r
                pending = r.discovered;
                r.discovered = null;
            } else {
                if (waitForNotify) {
                    lock.wait(); // 取不到则休眠
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield(); // 让出线程的CPU时间,这样希望能删除一些活动引用,使用GC回收一些空间
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r); // 若引用队列不为空,将对象放入ReferenceQueue队列中
    return true;
} 
 

2. ReferenceQueue

ReferenceQueue 的属性

static ReferenceQueue NULL = new Null<>();
static ReferenceQueue ENQUEUED = new Null<>();

private static class Null extends ReferenceQueue {
    boolean enqueue(Reference r) {
        return false;
    }
} 
 

入队

使用头插法,将引用 r 存入 r.queue 队列中。

java.lang.ref.ReferenceQueue#enqueue

boolean enqueue(Reference r) { /* Called only by Reference class */
    synchronized (lock) {
        // Check that since getting the lock this reference hasn't already been
        // enqueued (and even then removed)
        ReferenceQueue queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) { // 校验引用r是否已经存入ReferenceQueue之中,或者已经从ReferenceQueue中移除
            return false;
        }
        assert queue == this;
        r.queue = ENQUEUED; // 表示引用r已经存入ReferenceQueue之中
        r.next = (head == null) ? r : head; // 头插法。r.next = 队列的下一个节点
        head = r;
        queueLength++;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        lock.notifyAll();
        return true;
    }
}

出队

将头节点出队,设置新的头节点,并将引用 r 的下一个节点指向自身。

java.lang.ref.ReferenceQueue#reallyPoll

@SuppressWarnings("unchecked")
private Reference reallyPoll() {       /* Must hold lock */
    Reference r = head;
    if (r != null) {
        head = (r.next == r) ?
            null :
            r.next; // Unchecked due to the next field having a raw type in Reference // 头节点出队
        r.queue = NULL; // 表示引用r已经从ReferenceQueue中移除
        r.next = r;     // 自连接,方便回收
        queueLength--;
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        return r;
    }
    return null;
}

3. WeakReference

在 JDK 1.2 版之前,Java 里面的引用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用。在 JDK 1.2 版之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。

  • 强引用:无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
  • 弱引用:当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

WeakReference 的定义

java.lang.ref.WeakReference

public class WeakReference extends Reference {

    /**
     * Creates a new weak reference that refers to the given object.  The new
     * reference is not registered with any queue.
     *
     * @param referent object the new weak reference will refer to
     */
    public WeakReference(T referent) {
        super(referent);
    }

    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or null if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }

}    

WeakReference 的使用

/**
 * 弱引用:当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
 * 

* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * 堆大小为20m,其中新生代大小为10m,按照1:8比例分配,Eden区大小设置为8m * 此时存入8m大小的变量,直接存入老年代(tenured generation) */ @Test public void weakReference() { byte[] allocation01 = new byte[1024 * 1024 * 8]; WeakReference weakReference = new WeakReference(allocation01); System.out.println("weakReference.get() = " + weakReference.get());// [B@154ebadd allocation01 = null;// 解除强引用,只留下弱引用 System.out.println("weakReference.get() = " + weakReference.get());// [B@154ebadd System.gc(); System.out.println("weakReference.get() = " + weakReference.get());// null }

执行结果:

weakReference.get() = [B@f2a0b8e
[GC (System.gc())  15209K->9574K(19456K), 0.0209182 secs]
[Full GC (System.gc())  9574K->1323K(19456K), 0.0239549 secs]
weakReference.get() = null

WeakReference 和 ReferenceQueue 配合使用

@Test
public void referenceQueue() throws InterruptedException {
    // 创建一个引用队列
    ReferenceQueue referenceQueue = new ReferenceQueue();

    /**
     * 创建弱引用,此时 Reference 状态为 Active,
     */
    WeakReference weakReference = new WeakReference(new Object(), referenceQueue);
    System.out.println(weakReference);// java.lang.ref.WeakReference@f2a0b8e
    System.out.println(weakReference.get());// java.lang.Object@593634ad

    /**
     * 当 GC 执行后,由于自定了引用队列,Reference 的状态由 Pending 变为 Enqueued
     */
    System.gc();

    System.out.println(weakReference);// java.lang.ref.WeakReference@f2a0b8e
    System.out.println(weakReference.get());// null

    /**
     * 从队列里面取出该元素,Reference 状态为 Inactive
     */
    Reference reference = referenceQueue.remove();
    System.out.println(reference);// java.lang.ref.WeakReference@f2a0b8e
    System.out.println(reference.get());// null

    reference = referenceQueue.poll();
    System.out.println(reference);// null
}

for 循环中的引用对象

来看一下特殊的例子。

/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
@Test
public void weakReferenceInLoop01() throws InterruptedException {
    ReferenceQueue referenceQueue = new ReferenceQueue();
    for (int i = 0; i < 5; i++) {
        byte[] bytes = new byte[1024 * 1024 * 8]; // 虽然是个强引用,但是下一次循环后就变为不可达!
        WeakReference weakReference = new WeakReference(bytes, referenceQueue);
    }
    Reference remove = referenceQueue.remove(1000); // 这里没有得到通知!
    System.out.println("remove = " + remove); // null
}

执行结果:

[GC (Allocation Failure)  15241K->9594K(19456K), 0.0025132 secs]
[GC (Allocation Failure)  9594K->9610K(19456K), 0.0010068 secs]
[Full GC (Allocation Failure)  9610K->1328K(19456K), 0.0151334 secs]
[GC (Allocation Failure)  9520K->9520K(19456K), 0.0003440 secs]
[Full GC (Ergonomics)  9520K->1328K(19456K), 0.0055399 secs]
[GC (Allocation Failure)  9520K->9520K(19456K), 0.0002247 secs]
[Full GC (Ergonomics)  9520K->963K(19456K), 0.0068916 secs]
[GC (Allocation Failure)  9156K->9156K(19456K), 0.0003452 secs]
[Full GC (Ergonomics)  9156K->1024K(19456K), 0.0024345 secs]
remove = null

上面的例子有两个注意要点:

  1. for 循环中的 bytes,虽然是个强引用,但是下一次循环后就变为不可达,因此弱引用会被回收。
  2. 弱引用被回收,但是 ReferenceQueue 并没有得到通知。

即使在 java.lang.ref.Reference#tryHandlePending 中打断点,也没有进入。

为什么 ReferenceQueue 没有得到通知呢,来看下一个例子。

/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
@Test
public void weakReferenceInLoop02() throws InterruptedException {
    ReferenceQueue referenceQueue = new ReferenceQueue();
    WeakReference[] weakReferences = new WeakReference[10];
    for (int i = 0; i < 5; i++) {
        byte[] bytes = new byte[1024 * 1024 * 8];
        WeakReference weakReference = new WeakReference(bytes, referenceQueue);
        weakReferences[i] = weakReference;
    }
    Reference remove = referenceQueue.remove(1000); // 这里得到通知了!
    System.out.println("remove = " + remove);
}

执行结果:

[GC (Allocation Failure)  15196K->9597K(19456K), 0.0033007 secs]
[GC (Allocation Failure)  9597K->9629K(19456K), 0.0101304 secs]
[Full GC (Allocation Failure)  9629K->1328K(19456K), 0.0097783 secs]
[GC (Allocation Failure)  9520K->9552K(19456K), 0.0076307 secs]
[Full GC (Ergonomics)  9552K->1536K(19456K), 0.0299183 secs]
[GC (Allocation Failure)  9728K->9760K(19456K), 0.0082721 secs]
[Full GC (Ergonomics)  9760K->1328K(19456K), 0.0051265 secs]
[GC (Allocation Failure)  9520K->9552K(19456K), 0.0004645 secs]
[Full GC (Ergonomics)  9552K->1328K(19456K), 0.0044950 secs]
remove = java.lang.ref.WeakReference@f2a0b8e

这个例子中,for 循环中的 WeakReference 通过赋值给数组,暴露给 for 循环之外了。
如果在 java.lang.ref.Reference#tryHandlePending 中打断点,发现是可以进入断点位置的。

猜测是 JVM 的一种优化手段,当 WeakReference 对象本身有关联到强引用时,GC 回收弱引用的时候才会将其存入 pending-Reference 列表,以便后续由 Reference-handler 线程将其存入 ReferenceQueue 队列。

4. WeakHashMap

public class WeakHashMap
extends AbstractMap
implements Map

WeakHashMap 是以弱键实现的基于哈希表的 Map。在 WeakHashMap 中,当某个 key 不再正常使用时,将自动移除该 Entry。
支持 null 值和 null 键。该类具有与 HashMap 类相似的性能特征,并具有相同的初始容量(16)和加载因子(0.75)。
像大多数 collection 类一样,该类是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。

WeakHashMap 的数据结构

WeakHashMap 的存储结构只有(数组 + 链表)。
WeakHashMap 由于使用了弱引用,在 GC 时会回收没有强引用的 key,因此不会存储过多的元素,无需转换成树结构。

数组结构定义如下:

/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
Entry[] table;

WeakHashMap 中的内部类 Entry 继承了 WeakReference,将 key 交给 WeakReference 管理。

java.util.WeakHashMap.Entry

private static class Entry extends WeakReference implements Map.Entry {
    V value;
    final int hash;
    Entry next;

    Entry(Object key, V value,
          ReferenceQueue queue,
          int hash, Entry next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}     
 

定义了属性 ReferenceQueue

private final ReferenceQueue queue = new ReferenceQueue<>(); 
 

往 WeakHashMap 中添加元素时,使用new Entry<>(k, value, queue, h, e)创建 Entry 条目,传入 ReferenceQueue,具体见 WeakHashMap#put 方法。

put

java.util.WeakHashMap#put

public V put(K key, V value) {
    Object k = maskNull(key);
    int h = hash(k);
    Entry[] tab = getTable(); // 获取当前table数组(获取之前会清除废弃的元素)
    int i = indexFor(h, tab.length);

    for (Entry e = tab[i]; e != null; e = e.next) { // 遍历链表
        if (h == e.hash && eq(k, e.get())) {
            V oldValue = e.value;
            if (value != oldValue)
                e.value = value; // 替换旧值
            return oldValue;
        }
    }

    modCount++;
    Entry e = tab[i];
    tab[i] = new Entry<>(k, value, queue, h, e); // 头插法
    if (++size >= threshold)
        resize(tab.length * 2); // 扩容
    return null;
}

流程跟 HashMap#put 相比简化了不少,关注不同的地方:

  1. WeakHashMap 的构造函数中就创建了数组,而 HashMap 第一次 put 操作才初始化数组。
  2. getTable() 获取当前数组 table 时,会先清除 Key 已被回收的 Entry,再返回 table。
  3. 由于桶中没有树结构,定位到桶之后只需要遍历链表即可。
  4. 链表使用头插法加入新元素。

expungeStaleEntries

expungeStaleEntries() 方法用于清除废弃元素,getTable()size()resize()方法都会调用该方法。
基本上 WeakHashMap 的每个方法都会调用 getTable(),可知 expungeStaleEntries() 是 WeakHashMap 中核心的逻辑。

阅读 JDK 8 源码:WeakHashMap 和 Reference、ReferenceQueue_第3张图片

java.util.WeakHashMap#expungeStaleEntries

private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) { // 被回收的对象,会由GC存入引用队列中
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry e = (Entry) x;
            int i = indexFor(e.hash, table.length); // 被回收的节点所在桶的位置

            Entry prev = table[i]; // 被回收的节点所在桶的头节点
            Entry p = prev;
            while (p != null) { // 遍历链表,删除节点e
                Entry next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next; // 节点e的前一个节点,指向节点e的下一个节点,即在链表上解开节点e
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    e.value = null; // Help GC
                    size--; // 容量减少
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

代码流程:

  1. 从 ReferenceQueue 中拉取已被 GC 回收的弱引用节点
  2. 定位该节点在数组中所在桶的位置
  3. 遍历桶上的链表,删除该节点

resize

当满足 size >= threshold时,调用扩容方法。
需要注意的是,执行扩容之前,会清除 WeakHashMap 中废弃的 Entry,导致 size 变小而达不到扩容阈值。
此时 WeakHashMap 采取先扩容,再将新的 size 与旧的 threshold 进行对比。
若满足size >= threshold / 2说明扩容成功,更新阈值;否则放弃扩容,把元素迁移回旧数组。
先扩容又放弃扩容,反复迁移数组是一个比较低效的操作。

void resize(int newCapacity) {
    Entry[] oldTable = getTable(); // 扩容之前获取旧数组,该操作会触发清除废弃元素,导致 size 变小而达不到扩容阈值
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = newTable(newCapacity); // 扩容
    transfer(oldTable, newTable); // 迁移元素至新数组
    table = newTable;

    /*
     * If ignoring null elements and processing ref queue caused massive
     * shrinkage, then restore old table.  This should be rare, but avoids
     * unbounded expansion of garbage-filled tables.
     */
    if (size >= threshold / 2) { // 这里放宽了扩容阈值,为原来的一半,目的是避免反复迁移数组
        threshold = (int)(newCapacity * loadFactor); // 扩容完成,更新阈值
    } else { // 放弃扩容
        expungeStaleEntries(); // 清除废弃的元素
        transfer(newTable, oldTable); // 迁移元素至旧数组
        table = oldTable;
    }
}

WeakHashMap 使用案例

往 WeakHashMap 中存入 10000 个 key 为 1M 大小的元素,循环结束之后查得 WeakHashMap 大小远不到 10000。

@Test
public void weakHashMap() throws InterruptedException {
    WeakHashMap map = new WeakHashMap<>();
    Object value = new Object();
    for (int i = 0; i < 10000; i++) {
        byte[] bytes = new byte[1024 * 1024]; // 1M
        map.put(bytes, value);
    }
    System.out.println("map.size->" + map.size()); // 609
}

总结一下,WeakHashMap 使用了弱引用作为 key,当 GC 回收时,清除弱引用并将 Entry 存入 WeakHashMap 中内置的 ReferenceQueue 中。
后续 WeakHashMap 的每一步操作都会从 ReferenceQueue 中拉取得到 Entry,并从 WeakHashMap 中删除该 Entry。
根据这一特性,WeakHashMap 很适合作为缓存使用。

理解了 WeakHashMap 的原理,使用 HashMap 同样可以做到类似的效果。

@Test
public void hashMap() throws InterruptedException {
    ReferenceQueue referenceQueue = new ReferenceQueue(); // 定义引用队列
    Object value = new Object();
    HashMap map = new HashMap<>();
    for (int i = 0; i < 10000; i++) {
        byte[] bytes = new byte[1024 * 1024]; // 1M
        WeakReference weakReference = new WeakReference(bytes, referenceQueue);
        map.put(weakReference, value); // 使用WeakReference作为Key
    }
    System.out.println("map.size->" + map.size()); // 10000

    Thread thread = new Thread(() -> {
        try {
            int cnt = 0;
            WeakReference k;
            while ((k = (WeakReference) referenceQueue.remove(1000)) != null) {
                 map.remove(k); // 从HashMap中移除Entry,达到与WeakHashMap一样的效果
            }
        } catch (InterruptedException e) {
            // nothing
        }
    });
    thread.setDaemon(true);
    thread.start();
    thread.join();// 主线程需要等待,直到当前线程thread消亡

    System.out.println("map.size->" + map.size()); // 远不足 10000
}

5. 参考


作者:Sumkor
链接:https://segmentfault.com/a/11...

你可能感兴趣的:(java程序员jdk)