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
    extends AbstractMap
    implements Map {

  其实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 queue = new ReferenceQueue<>();

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

大家可以看下静态内部类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;
    }
}

  大家可以看到,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 e = (Entry) x;
            // 获取当前节点的索引位置
            int i = indexFor(e.hash, table.length);
            // 获取索引位置的节点
            Entry prev = table[i];
            Entry p = prev;
            // 判断节点是否存在
            while (p != null) {
                // p的下个节点
                Entry 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 q) {
    super(referent, q);
}
Reference(T referent, ReferenceQueue queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

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

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

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

public abstract class Reference {
   
    // GC线程在回收对象的时候的锁
    static private class Lock { }
    private static Lock lock = new Lock();
    // 存放被回收的引用对象,该对象受上面锁的保护
    private static Reference pending = null;
    // 引用队列,存放pending
    volatile ReferenceQueue 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 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 q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

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

例子

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

public static void main(String[] args) {
    Map 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 b = new WeakReference(a);
    WeakHashMap weakMap = new WeakHashMap();
    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的理解

你可能感兴趣的:(Java1.8-WeakHashMap源码解析)