Java1.8-WeakHashMap源码解析

概述

  在学习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在进行垃圾回收的时候能智能的回收那些无用的对象。

属性

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回收后,那么对应的引用就会加入到引用队列中。

方法

  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清除后的引用是什么时候进入引用队列的呢。

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的实现:

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了,所以被回收了。

使用场景

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

总结

  以上呢就是对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的理解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349