Java还要再学一遍基础(十一)WeakHashMap详解

xiaoxiao2021-02-28  70

WeakHashMap概述

WeakHashMap是以弱键实现的基于哈希表的存储映射数据的Map。当JVM对于这些弱键所指向的对象进行了清理回收之后,WeakHashMap会自动有效的将被回收了的映射从map中移除。

引用的相关知识

Java中的引用一共分为四种,分别为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和幻影引用(Phantom Reference)。 可以看到java.lang.ref包中有对应的几种:

强引用(Strong Reference) 强引用是java默认实现的引用。JVM会尽可能长时间的保留强引用的存在。当没有任何对象指向它时JVM将会回收。

//这是一个强引用 Object obj = new Object();

弱引用(Weak Reference) 弱引用是指当对象没有任何的强引用存在,在下一次的JVM的gc回收的时候它将会被回收。

//这里是一个强引用 Object obj = new Object(); //new一个弱引用 WeakReference<Object> weakRef = new WeakReference<Object>(obj); //obj = null后不再有强引用 obj = null; //因为jvm的gc时间不确定所以循环 while(true){ //weakRef.get()当还未被回收的时候返回该对象,否则返回null if(weakRef.get() != null){ System.out.println("is alive"); } else{ System.out.println("is not alive"); break; } }

运行结果:

... ... is alive is alive is alive is alive is alive is alive is not alive软引用(Soft Reference) 软引用和弱引用基本性质一致,区别就是软引用JVM只会在虚拟机内存不足的时候才会去回收软引用,这使得软引用很适合做缓存应用。幻影引用(Phantom Reference) 这种引用类型的get方法无论什么时候都会返回null。它唯一的作用就是可以用来记录对象是什么时候被gc回收的。

问题来了。怎么去记录对象的回收呢? 从上面的图片中包含的class可以看到其中有一个ReferenceQueue的class,翻译成中文就是引用队列,用来干什么? 原来java提供这个引用队列,可以把这个队列当作参数传到引用的构造器中,那么之后当对象被回收之后便会做一件事情,就是把被回收的对象放到这个因哟个队列中去。

利用ReferenceQueue记录对象被回收的例子:

Object obj = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); WeakReference<Object> weakRef = new WeakReference<Object>(obj, queue); System.out.println(obj); obj = null; System.gc(); while(true){ //已经被回收 if(weakRef.get() == null){ Object o; //从队列中取 if((o = queue.poll()) != null){ System.out.println(o); break; } } }

运行结果:

java.lang.Object@15db9742 java.lang.ref.WeakReference@6d06d69c

WeakHashMap

有了引用以及弱引用的相关概念并且了解HashMap的话WeakHashMap便很容易理解了。本文基于JDK1.8

1. 类定义

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

跟HashMap一样继承自AbstractMap并且实现Map接口,与之不同的是HashMap同时还实现了Cloneable, Serializable接口,这意味着WeakHashMap将不支持克隆和序列化。


2. 属性

//默认容量 private static final int DEFAULT_INITIAL_CAPACITY = 16; //最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子 private static final float DEFAULT_LOAD_FACTOR = 0.75f; //节点数组 Entry<K,V>[] table; //size private int size; //阈值 private int threshold; //加载因子 private final float loadFactor; //ReferenceQueue用于记录gc回收 private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); //修改标志 int modCount;

可以看到基本上和HashMap没有什么太大的出入,明显的区别则是多了一个ReferenceQueue,很明显WeakHashMap就是通过这个引用队列来实现自动清理的。


2. 构造方法

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; while (capacity < initialCapacity) capacity <<= 1; table = newTable(capacity); this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); } //其他构造参数基本与HashMap相同,省略

这里有一点点与HashMap不同的是,HashMap在确定capacity的算法上有些不同。HashMap是通五次的无符号右移并或运算

static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

而WeakHashMap则是通过while循环实现。 同时HashMap的table数组并没有直接在构造器里面初始化,而是在加入元素之后延迟初始化的。

3. 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; } //......

这里看看构造器,传入了一个ReferenceQueue,似乎根最开始讲的ReferenceQueue记录gc回收很相似的用法。因为Entry继承了WeakReference类,所以现在的Entry对象管理的并不是强引用,而是弱引用也就是WeakReference。所以当被回收的时候自然就将会被这个引用队列所记录下来。之后只需要进行清理操作即可。


3. 关键的方法

get方法。为了能让每次获取map中的元素的时候都能从已经自动清理过后的table数组中获取,所以get方法中加入了对应了清理操作。

public V get(Object key) { Object k = maskNull(key); int h = hash(k); //这个getTable便是获取清理后的table Entry<K,V>[] tab = getTable(); int index = indexFor(h, tab.length); Entry<K,V> e = tab[index]; while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; }

如何清理的:

private Entry<K,V>[] getTable() { //expunge是抹去的意思 Stale:陈旧的 expungeStaleEntries(); return table; } private void expungeStaleEntries() { //从队列中出队遍历 for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; //通过hash找到index int i = indexFor(e.hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> p = prev; //遍历链表 while (p != null) { //记录下一个节点next 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 e.value = null; // Help GC size--; break; } //p记录下一个节点用作遍历,pref则是p的上一个节点 prev = p; p = next; } } } }

这下就很清楚了,原来是遍历队列中被回收的记录,再在table数组以及处理hash冲突的链表中去找到该记录并删除。 初次之外,在resize和size方法调用的时候也使用到了上面的expungeStaleEntries方法。这里不做详细介绍。

hash方法

final int hash(Object k) { int h = k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }

和HashMap不相同,但是有点复杂….

WeakHashMap的特性

存储键值对映射。key和value可以为null线程不安全当键被gc回收能够自动清理map中的映射不支持克隆和序列化
转载请注明原文地址: https://www.6miu.com/read-37524.html

最新回复(0)