Java1.8-WeakHashMap源码解析

澳门新葡亰网站注册 2

get

调用Reference.get方法可以得到该引用指向的对象,但是由于指向的对象随时可能被
GC 清理,所以即使在同一个线程中,不同时刻的调用可能返回不一样的值。

使用场景

  至于WeakHashMap的使用场景,目前是在tomcat的ConcurrentCache中使用到了它。其他情况下使用的不多,不过了解了这个对象之后,对我们以后遇到问题的时候,未尝不是一种解决方案呢。

今天要介绍的WeakHashMap并没有基于某种特殊的数据结构,它的主要目的是为了优化JVM,使JVM中的垃圾回收器(garbage
collector,后面简写为 GC)更智能的回收“无用”的对象。

ReferenceHandler线程

我们可以通过Entry的结构看到,Entry是继承自WeakReference,而WeakReference是继承自Reference。
我们从Entry的构造方法开始看:

public WeakReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

可以看到,最终是进入了Reference抽象类。

  通过阅读Reference的文档,我们知道Reference对象是与垃圾回收器有直接的关联的。而这种直接的关联是通过ReferenceHandler
这个线程来实现的。ReferenceHandler线程是JVM创建main线程后创建的线程,其优先级最高,是10,它就是用来处理引用对象的垃圾回收问题的。

我们来介绍下Reference的一些变量和方法。

public abstract class Reference<T> {

    // GC线程在回收对象的时候的锁
    static private class Lock { }
    private static Lock lock = new Lock();
    // 存放被回收的引用对象,该对象受上面锁的保护
    private static Reference<Object> pending = null;
    // 引用队列,存放pending
    volatile ReferenceQueue<? super T> queue;

    // 静态内部类,ReferenceHandler线程,处理引用队列的高线程。
    // 在static块里面被初始化。该守护线程启动后,会处于等待状态,
    private static class ReferenceHandler extends Thread {
    }
}

我们可以大概看一下JVM进行GC时ReferenceHandler线程所做的工作:

  1. JVM在进行GC的时候,会创建ConcurrentMarkSweepThread线程(简称CMST)去执行GC,并且同时创建SurrogateLockerThread线程(简称SLT)。CMST开始GC时,会发一个消息给SLT让它去获取Java的Reference对象的全局锁:Lock。直到CMS
    GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference的pending属性当中,然后通知SLT释放并且notify全局锁:Lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。
  2. ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。
  3. 然后当我们操作WeakHashMap的时候,就会相应的处理引用队列中的这部分数据。

我们来看一下Reference中的静态代码块:

static {
    // 获取线程组
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    // 然后创建ReferenceHandler线程对象
    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();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

而ReferenceHandler中重载的run方法如下:

public void run() {
    while (true) {
        tryHandlePending(true);
    }
}
static boolean tryHandlePending(boolean waitForNotify) {
    // pending这里,在GC的时候,JVM在通过计算对象key的可达性后,发现没有该key对象的引用,就会把该对象关联的Entry添加到pending中。
// pending这里会涉及到线程的阻塞,如果pending为空,会阻塞当前线程
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                pending = r.discovered;
                r.discovered = null;
            } else {
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }
    // 将pending放入引用队列中
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

而到这里,我们也基本上明白了弱引用对象是通过什么方式进入引用队列的了。

WeakHashMap.expungeStaleEntries

/**
 * Reference queue for cleared WeakEntries
 */
// 所有Entry在构造时都传入该queue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
 * Expunges stale entries from the table.
 */
private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            // e 为要清理的对象
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            // while 循环遍历冲突链
            while (p != null) {
                Entry<K,V> next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    // 可以看到这里把value赋值为null,来帮助 GC 回收强引用的value
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

知道了expungeStaleEntries方法的作用,下面看看它是何时被调用的

澳门新葡亰网站注册 1

expungeStaleEntries调用链

可以看到,在对WeakHashMap进行增删改查时,都调用了expungeStaleEntries方法。

方法

  WeakHashMap中的大部分方法都和HashMap类似,由于没有红黑树的存在,大部分方法还是挺简单的,今天主要来看expungeStaleEntries这个方法,也就是WeakHashMap弱引用实现的关键方法。

/**
 * expungeStaleEntries方法就是在引用队列中寻找是否有被回收的key的引用,
 * 如果有,则在table数组中删掉其对应的映射。
 */
private void expungeStaleEntries() {
    // 遍历队列,通过队列的poll方法从队头获取数据,如果存在被GC的对象,就需要移除map中对应的数据
    for (Object x; (x = queue.poll()) != null; ) {
        // 线程同步该队列
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                // 队列中保存的就是Entry
                Entry<K,V> e = (Entry<K,V>) x;
            // 获取当前节点的索引位置
            int i = indexFor(e.hash, table.length);
            // 获取索引位置的节点
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            // 判断节点是否存在
            while (p != null) {
                // p的下个节点
                Entry<K,V> next = p.next;
                // 如果p就是当前节点
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    // 将value设为null,帮助GC回收
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

其中上述循环语句简单说两句:

  1. 获取到索引所在的链表。遍历链表;
  2. 如果这个链表的头节点就是当前节点,那么就把链表的下一个节点移到头节点,然后设置value为null,进行后续操作;
  3. 如果链表头节点不是当前节点,后续根据next进行遍历,挨个判断。如果查询到当前节点,设置value为null,进行后续操作。

  在WeakHashMap中,大部分方法都会直接或间接调用该方法,来进行清除已经被回收的key的映射操作。

现在我们可以总结一下WeakhashMap弱引用的大概原理了。

  1. 创建WeakHashMap,添加对应的键值对信息,而底层是使用一个数组来保存对应的键值对信息Entry,而Entry生成的时候就与引用队列ReferenceQueue进行了关联;
  2. 当某弱键key不再被其他对象使用,并被JVM回收时,这个弱键对应的Entry会被同时添加到引用队列中去。
  3. 当下一次我们操作WeakHashMap时(比如调用get方法),会先处理引用队列中的这部分数据,这样这些弱键值对就自动在WeakHashMap中被自动删除了。

那么,其实还有另一个问题:被GC清除后的引用是什么时候进入引用队列的呢。

构造函数

//referent 为引用指向的对象
Reference(T referent) {
    this(referent, null);
}
//ReferenceQueue对象,可以简单理解为一个队列
//GC 在检测到appropriate reachability changes之后,
//会把引用对象本身添加到这个queue中,便于清理引用对象本身
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

如果我们在创建一个引用对象时,指定了ReferenceQueue,那么当引用对象指向的对象达到合适的状态(根据引用类型不同而不同)时,GC
会把引用对象本身添加到这个队列中,方便我们处理它,因为

引用对象指向的对象 GC
会自动清理,但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。

举个例子:

SoftReference<String> ss = new SoftReference<String>("abc" , queue);

ss 为软引用,指向abc这个对象,abc 会在一定时机被 GC
自动清理,但是ss对象本身的清理工作依赖于queue,当ss出现在queue中时,说明其指向的对象已经无效,可以放心清理ss了。

从上面的分析大家应该对Reference类有了基本的认识,但是上面也提到了,不同的引用,添加到ReferenceQueue的时机是不一样。下面介绍具体引用时再进行说明。
这里有个问题,如果创建引用对象是没有指定ReferenceQueue,引用对象会怎么样呢?这里需要了解Reference类内部的四种状态。

属性

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {

  其实WeakHashMap中的继承体系和大部分常量都和HashMap没什么不同。在存储上的不同点,或许是WeakHashMap解决index冲突仍旧使用的是链表,并没有使用红黑树。大概有以下特性:

  1. 根据API文档,当Map中的键不再使用,键对应的键值也将自动在WeakHashMap中删除。WeakHashMap中的键为弱键,和其他Map接口的实现有些不同;
  2. 和HashMap类似,支持key和value为null;
  3. 同样不是线程安全的,可以使用Collections.synchronizedMap来使之线程安全;
  4. 没有实现Cloneable, Serializable接口;
// 比HashMap少了一些属性,但多了一个弱键的引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

  该引用队列,用于存放虚拟机回收的Entry的引用,也就是说,一旦GC之后有key被清除,那key对应的引用就会被放入引用队列中。

大家可以看下静态内部类Entry:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    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;
    }
}

  大家可以看到,Entry继承了WeakReference,所以Entry是个弱引用类型。Entry生成的时候就将与ReferenceQueue绑定,这样我们就可以实现对弱引用的监听,一旦JVM回收后,那么对应的引用就会加入到引用队列中。

弱引用(weak reference)

软引用“保存”对象的能力稍逊于弱引用,但是高于虚引用,一般用来实现canonicalizing
mapping,也就是本文要讲的WeakHashMap

当弱引用指向的对象只能通过弱引用(没有强引用或弱引用)访问时,GC会清理掉该对象,之后,引用对象会被放到ReferenceQueue中。

例子

我们通过一个简单的例子来看一下WeakHashMap的实现:

public static void main(String[] args) {
    Map<String, String> weakMap = new WeakHashMap<>();
    weakMap.put(new String("1"), "1");
    weakMap.put(new String("2"), "2");
    weakMap.put(new String("3"), "3");
    weakMap.put("4", "4");
    String five = new String("5");
    weakMap.put(five, "5");
    System.out.println("weakMap.size:" + weakMap.size());
    //手动触发 GC
    System.gc();
    try {
        Thread.sleep(50);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("=============");
    System.out.println("weakMap:" + weakMap);
    System.out.println("weakMap.size:" + weakMap.size());
}

澳门新葡亰网站注册,总共放入Map中5个对象,我们运行一下结果:

weakMap.size:5
=============
weakMap:{4=4, 5=5}
weakMap.size:2

可以看到,Map里只剩下后两个对象了。接下来,我们稍微改动下代码:

// 我们在put第5个对象之后,将five设置为null,再看下打印结果
weakMap.put(five, "5");
five = null;

weakMap.size:5
=============
weakMap:{4=4}
weakMap.size:1

可以看到,第5个对象也被回收掉了。
从上面可以知道,数据1,2,3因为没有其他的引用,所以会被GC进行回收,而数据4是常量,是放在常量池中的,一般是不会被GC进行回收的。对于数据5,因为指向的引用为null了,所以被回收了。

虚引用(phantom reference)

虚引用是“保存”对象能力最弱的引用,一般用来实现scheduling pre-mortem
cleanup actions in a more flexible way than is possible with the Java
finalization mechanism

调用虚引用的get方法,总会返回null,与软引用和弱引用不同的是,虚引用被enqueued时,GC
并不会自动清理虚引用指向的对象,只有当指向该对象的所有虚引用全部被清理(enqueued后)后或其本身不可达时,该对象才会被清理。

总结

  以上呢就是对WeakHashMap的一点浅显的认识了,等有时间了再来深入研究下,简单总结下:

  1. 弱引用对象是由ReferenceHandler守护线程来不断的进行enqueue操作(入队);
  2. 当我们操作WeakHashMap的时候,并不是WeakHashMap自动删除引用队列的引用,而是我们通过间接的调用expungeStaleEntries方法来实现的。

  最后的最后,抛出网上的一道面试题,我觉得这个面试题挺有意思的,既考察了WeakHashMap的使用,又考察了try-catch-finally-return这个点的掌握。

// 求最终打印结果
private static String test(){
    String a = new String("a");
    WeakReference<String> b = new WeakReference<String>(a);
    WeakHashMap<String, Integer> weakMap = new WeakHashMap<String, Integer>();
    weakMap.put(b.get(), 1);
    a = null;
    System.gc();
    String c = "";
    try{
        c = b.get().replace("a", "b");
        return c;
    }catch(Exception e){
        c = "c";
        return c;
    }finally{
        c += "d";
        return c + "e";
    }
}

本文参考了:
Java
内部线程
Java中的WeakHashMap实现分析

面试题地址:
#Java中关于WeakReference和WeakHashMap的理解

四种状态

每一时刻,Reference对象都处于下面四种状态中。这四种状态用Reference的成员变量queuenext(类似于单链表中的next)来标示。

ReferenceQueue<? super T> queue;
Reference next;

Active。新创建的引用对象都是这个状态,在 GC
检测到引用对象已经到达合适的reachability时,GC
会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。在这个状态中

//如果构造参数中没指定queue,那么queue为ReferenceQueue.NULL,否则为构造参数中传递过来的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null

Pending。pending-Reference列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。在这个状态中

//构造参数参数中传递过来的queue
queue = ReferenceQueue
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this

Enqueued。调用ReferenceQueue.enqueued方法后的引用处于这个状态中。没有注册的实例不会进入这个状态。在这个状态中

queue = ReferenceQueue.ENQUEUED
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this

Inactive。最终状态,处于这个状态的引用对象,状态不会在改变。在这个状态中

queue = ReferenceQueue.NULL
next = this

有了这些约束,GC
只需要检测next字段就可以知道是否需要对该引用对象采取特殊处理

  • 如果nextnull,那么说明该引用为Active状态
  • 如果next不为null,那么 GC 应该按其正常逻辑处理该引用。

我自己根据Reference.ReferenceHandler.runReferenceQueue.enqueue这两个方法,画出了这四种状态的转移图,供大家参考:

澳门新葡亰网站注册 2

Reference状态转移图

要理解这个状态 GC 到底做了什么事,需要看 JVM
的代码,我这里时间、能力都不够,就不献丑了,后面有机会再来填坑。
对于一般程序员来说,这四种状态完全可以不用管。最后简单两句话总结上面的四种状态:

  1. 如果构造函数中指定了ReferenceQueue,那么事后程序员可以通过该队列清理引用
  2. 如果构造函数中没有指定了ReferenceQueue,那么 GC 会自动清理引用

概述

  在学习WeakHashMap之前,先简单来说一下Java中的4种引用类型,它们分别是:强引用(Strong
Reference),软引用(Soft Reference),弱引用(Weak
Reference),幽灵引用或者翻译为虚引用(Phantom Reference)。

  1. 强引用:强引用是Java中最普遍的应用,比如new
    Object,新建的object对象就属于强引用类型。如果一个对象是强引用类型,那么即使是Java虚拟机内存空间不足时,GC也不会回收该对象,而是内存溢出,比如我们常见的OutOfMemoryError错误。
  2. 软引用:软引用是强度仅次于强引用的一种类型,它使用类SoftReference来表示。当虚拟机内存足够时,是不会回收这些软引用对象的。而当虚拟机内存不足时,GC会回收那些被软引用指向的对象。如果释放完这些对象后,虚拟机仍然内存不足,这时候才会抛出OutOfMemoryError错误。所以说软引用适合用于创建缓存,因为缓存中的对象相比其他对象,在内存不足的时候是可以释放掉的,而Mybatis中就有它的身影。
  3. 弱引用:弱引用在强度上又弱于软引用,它使用类WeakReference来表示。它相比软引用而言,拥有更短暂的生命周期。它可以引用一个对象,但并不阻止该对象被GC回收。在垃圾回收的时候,不管内存是否充足,如果一个对象的所有引用都是弱引用,那么该对象就会被回收。所以说,弱引用的对象的生命周期是两次GC之间的这段时间,也就是说其生命周期只存在于一个垃圾回收周期内,只能存活到下次GC之前;
  4. 幽灵引用:虚引用,形同虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象是虚引用了对象,那么这个引用有和没有是差不多的,在任何时候都可以被垃圾回收器回收。而虚引用主要用来跟踪对象被垃圾回收器回收的过程的,比如说程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。虚引用必须和引用队列一起使用。
  5. 引用队列:引用队列是一种属于监听性质的结构。比如说,一个对象的状态发生了变化,从强引用变为了弱引用,而引用队列就是用于获取这些引用信息的队列,并在合适的时候对这些引用做处理。

  简单说了下Java中的4种引用,因为这不是本篇文章的重点,等以后有时间了再仔细研究一下这几种引用。现在我们开始学习WeakHashMap。

WeakHashMap是一种基于Java的弱引用的哈希表实现。它的目的和常规的Map实现有些不同,它主要是用于优化JVM,使JVM在进行垃圾回收的时候能智能的回收那些无用的对象。

强引用

这是最常用的引用类型,在执行下面的语句时,变量 o 即为一个强引用。

Object o = new Object();

强引用指向的对象无论在何时,都不会被GC 清理掉。

一般来说,对于常驻类应用(比如server),随着时间的增加,所占用的内存往往会持续上升,如果程序中全部使用强引用,那么很容易造成内存泄漏,最终导致Out Of Memory (OOM),所以
Java
中提供了除强引用之外的其他三种引用,它们全部位于java.lang.ref包中,下面一一介绍。