昨天刚刚讲解了Java中的引用和引用队列,今天就趁热打铁,讲讲WeakHashMap的弱键回收机制。
目录:
- WeakHashMap介绍
- WeakHashMap例子
- WeakHashMap的使用场景
- WeakHashMap的数据结构
- WeakHashMap的弱键回收
1. WeakHashMap介绍
WeakHashMap继承AbstractMap,实现了Map接口。和HashMap一样,WeakHashMap也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
不过WeakHashMap的键是"弱键"。在WeakHashMap中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
WeakHashMap内部是通过弱引用来管理entry
的,弱引用的特性对应到WeakHashMap上意味着什么呢?将一对key, value
放入到WeakHashMap里并不能避免该key
值被GC回收,除非在WeakHashMap之外还有对该key
的强引用。
和HashMap一样,WeakHashMap是不同步的。可以使用Collections.synchronizedMap方法来构造同步的WeakHashMap。
2. WeakHashMap例子
public class TestWeakHashMap {
public static void main(String[] args) {
WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(10);
String key0 = new String("kuang");
String key1 = new String("zhong");
String key2 = new String("wen");
// 存放元素
weakHashMap.put(key0, "q1");
weakHashMap.put(key1, "q2");
weakHashMap.put(key2, "q3");
System.out.printf("weakHashMap: %s\n", weakHashMap);
// 是否包含某key
System.out.printf("contains key kuang : %s\n", weakHashMap.containsKey(key0));
System.out.printf("contains key zhong : %s\n", weakHashMap.containsKey(key1));
// 是否包含某value
System.out.printf("contains value 0 : %s\n", weakHashMap.containsValue(0));
// 移除key
weakHashMap.remove(key2);
System.out.printf("weakHashMap after remove: %s", weakHashMap);
// 这意味着"弱键"key0再没有被其它对象引用,调用gc时会回收WeakHashMap中与key0对应的键值对
key0 = null;
// 内存回收,这里会回收WeakHashMap中与"key0"对应的键值对
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 遍历WeakHashMap
Iterator iter = weakHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry en = (Map.Entry) iter.next();
System.out.printf("next : %s - %s\n", en.getKey(), en.getValue());
}
// 打印WeakHashMap的实际大小
System.out.printf("after gc WeakHashMap size: %s\n", weakHashMap.size());
}
}
执行输出:
weakHashMap: {wen=q3, zhong=q2, kuang=q1}
contains key kuang : true
contains key zhong : true
contains value 0 : false
weakHashMap after remove: {zhong=q2, kuang=q1}next : zhong - q2
after gc WeakHashMap size: 1
上面的例子展示了WeakHashMap的增删改查,以及弱键的回收,可以看到把Key的引用置为null,gc后,会将该键值对回收。
3. WeakHashMap的使用场景
一般用做缓存,比如Tomcat的源码里,实现缓存时会用到WeakHashMap,在缓存系统中,使用WeakHashMap可以避免内存泄漏,但是使用WeakHashMap做缓存时要注意,如果只有它的key只有WeakHashMap本身在用,而在WeakHashMap之外没有对该key
的强引用,那么GC时会回收这个key对应的entry。所以WeakHashMap不能用做主缓存,合适的用法应该是用它做二级的内存缓存,即那么过期缓存数据或者低频缓存数据。
public final class ConcurrentCache<K,V> {
private final int size;
private final Map<K,V> eden;
private final Map<K,V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
synchronized (longterm) {
v = this.longterm.get(k);
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= size) {
synchronized (longterm) {
this.longterm.putAll(this.eden);
}
this.eden.clear();
}
this.eden.put(k, v);
}
}
源码中有eden和longterm的两个map,对jvm堆区有所了解的话,可以猜测出tomcat在这里是使用ConcurrentHashMap和WeakHashMap做了分代的缓存。在put方法里,在插入一个k-v时,先检查eden缓存的容量是不是超了。没有超就直接放入eden缓存,如果超了则锁定longterm将eden中所有的k-v都放入longterm。再将eden清空并插入k-v。在get方法中,也是优先从eden中找对应的v,如果没有则进入longterm缓存中查找,找到后就加入eden缓存并返回。
经过这样的设计,相对常用的对象都能在eden缓存中找到,不常用(有可能被销毁的对象)的则进入longterm缓存。而longterm的key的实际对象没有其他引用指向它时,gc就会自动回收heap中该弱引用指向的实际对象,弱引用进入引用队列。longterm调用expungeStaleEntries()方法,遍历引用队列中的弱引用,并清除对应的Entry,不会造成内存空间的浪费。
4. WeakHashMap的数据结构
前面已经大概了解了WeakHashMap,接下来来分析WeakHashMap的源码,先从它的数据结构开始。
- 4.1 类的定义
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {
}
java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.WeakHashMap<K, V>
- 4.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;
private int size;
private int threshold;
private final float loadFactor;
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
int modCount;
- DEFAULT_INITIAL_CAPACITY : 初始容量
- MAXIMUM_CAPACITY : 最大容量
- DEFAULT_LOAD_FACTOR : 默认加载因子
- table : Entry数组
- size : 实际存放的数据个数
- threshold : 扩容阈值
- loadFactor : 加载因子
- queue : 引用队列
- modCount : 修改次数
这些和HashMap,HashTable类似,关于这些,可以看看我这篇文章:Java篇 - 并发容器之Hashtable源码分析
- 4.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;
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;
}
...
}
可以看到Entry类实现了Map.Entry接口,继承弱引用(WeakReference),属性有key,value和next引用。
构造器中需要传入一个引用队列,方法主要看getKey():
return (K) WeakHashMap.unmaskNull(get());
再来看看WeakHashMap的静态方法unmaskNull():
private static final Object NULL_KEY = new Object();
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
判断key是否等于NULL_KEY来选择是否返回null。
- 4.4 类关系图
5. WeakHashMap的弱键回收
上面看完WeakHashMap的数据结构,那么WeakHashMap是如何实现弱键回收的呢?其实根据前面的文章也能猜到,利用Reference和ReferenceQueue。
- 5.1 put数据
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
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;
}
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
- 判断key是否为null,为null的话将key赋值为NULL_KEY。
- 计算key的hash值,然后根据hash值查找待插入的位置。
- 遍历Entry数组,看该键是否已存在,存在的话则替换旧值,并返回旧值。
- 不存在则构建Entry对象存入数组。
这个流程和HashMap,HashTable等差不多。
- 5.2 get数据
public V get(Object key) {
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) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
一句话,先根据key的hash值找到索引位置,然后拿到Entry对象,再判断该Entry是否存在下一个引用(即hash碰撞),遍历该单链表,比较value值。
- 5.3 弱键如何回收?
根据上面的分析就很容易得出了,WeakHashMap内部的数据存储是用Entry[]数组,即键值对数组。Entry类继承于WeakReferece(弱引用),弱引用的特点再重申下:当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
在构造一个Entry对象的时候,会传入一个ReferenceQueue,key为弱引用包裹的对象:
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
再来看看WeakHashMap如何清理已经被回收的key的,被回收的key会存放在引用队列中:
private void expungeStaleEntries() {
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
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
遍历引用队列,然后删除已被回收的键值对(从数组移除,改变单链表结点引用,将value赋值为null),该方法会在WeakHashMap增删改查、扩容的地方调用。
因为一个key-value存放到WeakHashMap中后,key会被用弱引用包起来存储,如果这个key在WeakHashMap外部没有强引用的话,GC时将被回收,然后WeakHashMap根据引用队列对已回收的key做清理。