HashMap中如果一个键指向的对象没有其他任何引用指向它,也不会被GC回收,被map的桶接管了,存在强引用,只能手动remove。如果想要这种键值对被自动回收怎么办?带着问题看一下WeakHashMap的源码
WeakHashMap的属性
和HashMap中基本一样,不过WeakHashMap没有红黑树相关操作,还多了个ReferenceQueue。
ReferenceQueue存的是什么呢?这里可以推出是那些指向被回收的对象的弱引用。
/**
* The default initial capacity -- MUST be a power of two.
*/
private static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
Entry<K,V>[] table;
/**
* The number of key-value mappings contained in this weak hash map.
*/
private int size;
/**
* The next size value at which to resize (capacity * load factor).
*/
private int threshold;
/**
* The load factor for the hash table.
*/
private final float loadFactor;
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
* The number of times this WeakHashMap has been structurally modified.
* Structural modifications are those that change the number of
* mappings in the map or otherwise modify its internal structure
* (e.g., rehash). This field is used to make iterators on
* Collection-views of the map fail-fast.
*
* @see ConcurrentModificationException
*/
int modCount;
这里主要要看一下Entry<K,V>[] table,注释中写的是The table, resized as necessary.明显是桶数组。其中的每个元素都是Entry<K,V>类型的,也就是说链表的节点就是Entry<K,V>类型的。
看一下Entry<K,V>的定义:
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
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;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
public V getValue() {
return value;
}
public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
K k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
V v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public int hashCode() {
K k = getKey();
V v = getValue();
return Objects.hashCode(k) ^ Objects.hashCode(v);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
分析出几点:
- Entry<K,V> extends WeakReference说明Entry<K,V>本身是一个弱引用,那它的构造函数一定能返回一个弱引用,指向的是传给父类构造函数的第一个参数。同时绑定了一个引用队列。当指向的对象被回收后,弱引用就入队。
- Entry<K,V> 中包含一个属性Entry<K,V> next,next指向的是下一个Entry<K,V>对象,符合链表的定义。
- 第一次见到引用自身还能携带属性的,其中一个属性next又指向了同类的另一个引用,感觉有点绕。
偷来别人的图看一下:
WeakHashMap的构造函数
有两个参数,table的初始容量,加载因子,生成了table数组,元素类型是Entry<K,V>。
一般情况下用无参构造就行了。
/**
* Constructs a new, empty <tt>WeakHashMap</tt> with the given initial
* capacity and the given load factor.
*
* @param initialCapacity The initial capacity of the <tt>WeakHashMap</tt>
* @param loadFactor The load factor of the <tt>WeakHashMap</tt>
* @throws IllegalArgumentException if the initial capacity is negative,
* or if the load factor is nonpositive.
*/
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);
}
public WeakHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public WeakHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
private Entry<K,V>[] newTable(int n) {
return (Entry<K,V>[]) new Entry<?,?>[n];
}
WeakHashMap的一些工具方法
private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
private static boolean eq(Object x, Object y) {
return x == y || x.equals(y);
}
/**理解为HashMap中的扰动函数就行
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits.
*/
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);
}
/**返回table中的下标
* Returns index for hash code h.
*/
private static int indexFor(int h, int length) {
return h & (length-1);
}
关键方法expungeStaleEntries,作用是移除Entry<K,V>。
这些Entry<K,V>有一个共同的特征:
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
//取出已经入队的弱引用x (Entry<K,V> x)
for (Object x; (x = queue.poll()) != null; ) {
//一整个是一个同步块
synchronized (queue) {
@SuppressWarnings("unchecked")
//现在e是出队的那个弱引用,指向的是一个原本有强引用key指向的对象
Entry<K,V> e = (Entry<K,V>) x;
//e里面的属性hash配合table的长度来确定e当前在哪个桶
int i = indexFor(e.hash, table.length);
//prev是桶的第一个元素
Entry<K,V> prev = table[i];
//经典的链表双指针遍历法
Entry<K,V> p = prev;
//p往前走,prev跟在后面,直到p==e为止
while (p != null) {
Entry<K,V> next = p.next;
//找到了等于e的那个节点p
if (p == e) {
//这里说明e就是桶里的第一个元素
if (prev == e)
//直接置为next,e就从map中移除了
table[i] = next;
else
//不是第一个元素,跳过e
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
//到上面为止就已经完成了移除e
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
e这个东西是从引用队列里拿出来的,是一个弱引用,是对原本key指向的实际对象的一个弱引用。正是由于实际对象只剩这一个弱引用才会被GC回收。
然后e又是map中的一个元素,它本身是被强引用的,所以才有expungeStaleEntries()方法来从map中删除它的一个过程。
这里留了两个问题想不清楚:
- Must not null out e.next 是什么意思
- e.value = null; // Help GC 为什么要这样写
WeakHashMap的基操
PUT
public V put(K key, V value) {
//null也可以存进来
Object k = maskNull(key);
//计算key的hash
int h = hash(k);
//getTable()返回table,返回之前要expungeStaleEntries
Entry<K,V>[] tab = getTable();
//计算出table中的下标
int i = indexFor(h, tab.length);
//遍历链表,如果找到key一样的,并且值不相等的,给新值
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];
//插在了链表头,next指向原来的第一个元素
tab[i] = new Entry<>(k, value, queue, h, e);
//判断size是否达到扩容条件
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
RESIZE
void resize(int newCapacity) {
//拿到老表
Entry<K,V>[] oldTable = getTable();
//老表长度看一下
int oldCapacity = oldTable.length;
//如果老表长度等于最大容量
if (oldCapacity == MAXIMUM_CAPACITY) {
//门槛设置为最大,这样put操作就不会总是进resize方法了
threshold = Integer.MAX_VALUE;
//扩不了了,结束
return;
}
//来张新表
Entry<K,V>[] newTable = newTable(newCapacity);
//元素迁移
transfer(oldTable, newTable);
table = newTable;
/*
* If ignoring null elements and processing ref queue caused massive
* shrinkage, then restore old table. This should be rare, but avoids
* unbounded expansion of garbage-filled tables.
*/
//完成扩容之后,如果size大于门槛的1/2,门槛就按正常的设置为新的门槛
//如果size不大于门槛的1/2,(size明显缩水了,可能是其他操作中的
//expungeStaleEntries导致的,transfer里面也可能导致size缩水
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
}
TRANSFER
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
//老表中的所有元素搬到新表
for (int j = 0; j < src.length; ++j) {
//j号桶的第一个元素
Entry<K,V> e = src[j];
//老表的j号桶直接清空
src[j] = null;
while (e != null) {
//拿到e的下一个元素
Entry<K,V> next = e.next;
//对e做一些处理,e.get返回弱引用指向的实际对象,也就是key
Object key = e.get();
//如果key什么都没拿到
if (key == null) {
//e不要了,把它的next和value都给null size--
e.next = null; // Help GC
e.value = null; // " "
size--;
} else {
//重新计算元素在数字中的下标
int i = indexFor(e.hash, dest.length);
//头插法
e.next = dest[i];
dest[i] = e;
}
//循环插入下一个节点
e = next;
}
}
}
这里又出现了Help GC的问题,不是很懂。
为什么这里可以写e.next = null了?而expungeStaleEntries方法里不写呢(Must not null out e.next说的是不是e.next不可以为null?)
containsValue
public boolean containsValue(Object value) {
if (value==null)
return containsNullValue();
//主要看一下遍历方式:先数组,再链表
Entry<K,V>[] tab = getTable();
for (int i = tab.length; i-- > 0;)
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
get
public V get(Object key) {
//key为null也能查出来,NULL_KEY是一个 static final,计算出的hash总是一样
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);
Entry<K,V> e = tab[index];
//找到对应的桶,走一下链表
while (e != null) {
//看一下这里是怎么拿到key的,key不是e的属性,而是e弱引用的对象,直接get
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
总结
- 关键是理解Entry<K,V>的结构,WeakHashMap的用法。
- 理清引用关系很重要。
- 留下了一个问题:expungeStaleEntries()里面e.value = null; // Help GC是如何help GC的?为什么不能e.next=null?(猜测可能和stale entries may be in use by a HashIterator这一句有关)