Java之WeakHashMap源码浅析
作者:澄风
定义
从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用。
- 强引用:
- 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。 比如String str = "hello"这时候str就是一个强引用。
- 软引用:
- 内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
- 弱引用:
- 如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
- 虚引用:
- 如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。 使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。
源码解析
看下WeakHashMap 的构造函数
public WeakHashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Initial Capacity: "+ initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load factor: "+ loadFactor); int capacity = 1; // 保证容量是2的整数倍,有助于hash运算 while (capacity < initialCapacity) capacity <<= 1; // 初始化table数组 table = newTable(capacity); this.loadFactor = loadFactor; // 阀值 threshold = (int)(capacity * loadFactor); }
没什么好说的 table 是一个 Entry数组 Entry<K,V>[] table; newTable会初始化一个数组数组的容量就是前面计算出来的capacity,其值为2的整数次方。
HashMap的容量为什么是2的n次方?HashMap是如何保证容量是2的n次方的? HashMap容量取2的n次方,主要与hash寻址有关。在put(key,value)时,putVal()方法中通过i = (n - 1) & hash来计算key的散列地址。其实,i = (n - 1) & hash是一个%操作。也就是说,HashMap是通过%运算来获得key的散列地址的。但是,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立。这也就是为什么HashMap的容量需要保持在2的n次方了。
再看下Entry的类定义
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { V value; final int hash; Entry<K,V> next; /** * Creates new entry. */ 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的,我们结合WeakReference再看一下:
public class WeakReference<T> extends Reference<T> { /** * 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 <tt>null</tt> if registration is not required */ public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
我们发现在 weakhashmap 中把key注册给了 WeakReference ,也就是说在 WeakHashMap 中key是一个弱引用。但这个queue是什么我们接着看,在往 WeakHashMap 中put一个元素的时候,会创建Entry。再看 WeakHashMap 的put操作,我们如果熟悉 HashMap 其实我们不需要怎么看这部分的代码,无非是计算hash值,散列分布到数组的各个位置,如果 hash 冲突使用拉链法进行解决。这里和hashmap有一点不一样的是hashmap如果链长达到阀值会使用红黑树。
public V put(K key, V value) { // 如果key是null则给定一个空的对象进行修饰 Object k = maskNull(key); // 计算key的hash int h = hash(k); // 获取table Entry<K,V>[] tab = getTable(); // 根据hash找到数组下标 int i = indexFor(h, tab.length); // 找到链表中元素位置 for (Entry<K,V> 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<K,V> e = tab[i]; tab[i] = new Entry<>(k, value, queue, h, e); if (++size >= threshold) // 是否达到阀值达到阀值就扩容 resize(tab.length * 2); return null; }
可以看到这个queue是一个实例化final修饰的属性。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
再看下getTable是什么情况,看源码会知道所有的WeekHashMap的所有操作都要调用 getTable -> expungeStaleEntries
private Entry<K,V>[] getTable() { expungeStaleEntries(); return table; }
我们看下expungeStaleEntries做了哪些事情?
private void expungeStaleEntries() { // 从 ReferenceQueue中拉取元素 for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @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 (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 // 拿到entry的值赋值为null帮助GC e.value = null; // Help GC size--; break; } prev = p; p = next; } } } }
expungeStaleEntries 就是WeakHashMap的核心了,它承担着Map中死对象的清理工作。原理就是依赖WeakReference和ReferenceQueue的特性。
在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry了。
这里也是整个WeakHashMap里唯一加了同步的地方。除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。
WeakHashMap的一点点缺点
提到缺点我不太认为是缺点,在某种场景下缺点也有可能是优点,而且很多缺点也是可以弥补的。
但非要说个一二三,这里列出下面两种:
1.非线程安全
关键修改方法没有提供任何同步,多线程环境下肯定会导致数据不一致的情况,所以使用时需要多注意。
2.单纯作为Map没有HashMap好
HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。
WeakHashMap可以应用的地方
1.缓存
2.诊断工具,比如atlas,将字节码缓存放入到WeakHashMap中
到此这篇关于Java之WeakHashMap源码浅析的文章就介绍到这了,更多相关WeakHashMap源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!