Java中的WeakHashMap
楔子
WeakHashMap
,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值,所以比较适合做缓存。
WeakHashMap
的这种特性比较适合实现类似本地、堆内缓存的存储机制——缓存的失效依赖于GC收集器的行为
WeakHashMap
的定义如下:
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
简单来说,WeakHashMap
实现了Map接口,基于hash-table
实现,在这种Map中,key的类型是WeakReference
。如果对应的key被回收,则这个key指向的对象会被从Map容器中移除。
WeakHashMap
跟普通的HashMap不同,WeakHashMap的行为一定程度上基于垃圾收集器的行为,因此一些Map数据结构对应的常识在WeakHashMap上会失效——size()方法的返回值会随着程序的运行变小,isEmpty()方法的返回值会从false变成true等等。
实例
此例子中声明了两个Map对象,一个是HashMap,一个是WeakHashMap,同时向两个map中放入a、b两个对象,当HashMap remove掉a 并且将a、b都指向null时,WeakHashMap中的a将自动被回收掉。
出现这个状况的原因是,对于a对象而言,当HashMap remove掉并且将a指向null后,除了WeakHashMap中还保存a外已经没有指向a的指针了,所以WeakHashMap会自动舍弃掉a,而对于b对象虽然指向了null,但HashMap中还有指向b的指针。
弱引用(WeakReference
)的特性是:当gc线程发现某个对象只有弱引用指向它,那么就会将其销毁并回收内存。
package com.sino.daily.code_2019_9_1;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
/**
* create by 2020-04-28 22:45
*
* @author caogu
*/
public class WeekHash {
public static void main(String[] args) {
String a = new String("a");
String b = new String("b");
Map map = new HashMap();
map.put(a, "aaa");
map.put(b, "bbb");
Map weakmap = new WeakHashMap();
weakmap.put(a, "aaa");
weakmap.put(b, "bbb");
map.remove(a);
a = null;
b = null;
System.gc();
Iterator i = map.entrySet().iterator();
while (i.hasNext()) {
Map.Entry en = (Map.Entry) i.next();
System.out.println("map:" + en.getKey() + ":" + en.getValue());
}
Iterator j = weakmap.entrySet().iterator();
while (j.hasNext()) {
Map.Entry en = (Map.Entry) j.next();
System.out.println("weakmap:" + en.getKey() + ":" + en.getValue());
}
}
}
典型使用场景: tomcat两级缓存
tomcat的源码里,实现缓存时会用到WeakHashMap
package org.apache.tomcat.util.collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
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,不会造成内存空间的浪费。
利用WeakHashMap实现内存缓存
可以看出,WeakHashMap的这种特性比较适合实现类似本地、堆内缓存的存储机制——缓存的失效依赖于GC收集器的行为。假设一种应用场景:我们需要保存一批大的图片对象,其中values是图片的内容,key是图片的名字,这里我们需要选择一种合适的容器保存这些对象。
使用普通的HashMap并不是好的选择,这些大对象将会占用很多内存,并且还不会被GC回收,除非我们在对应的key废弃之前主动remove掉这些元素。WeakHashMap非常适合使用在这种场景下,下面的代码演示了具体的实现:
WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image"); //强引用
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
imageName = null; //map中的values对象成为弱引用对象
System.gc(); //主动触发一次GC
await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);
首先,创建一个WeakHashMap对象来存储BigImage实例,对应的key是UniqueImageName对象,保存到WeakHashMap里的时候,key是一个弱引用类型。
然后,我们将imageName设置为null,这样就没有其他强引用指向bigImage对象,按照WeakHashMap的规则,在下一次GC周期中会回收bigImage对象。
通过System.gc()主动触发一次GC过程,然后可以发现WeakHashMap成为空的了。
强引用、软引用和弱引用
- 强引用(
Strong Reference
) - 软引用(
Soft Reference
) - 弱引用 (
WeakReference
)
强引用
被强引用指向的对象,绝对不会被垃圾收集器回收。Integer prime = 1;
,这个语句中prime对象就有一个强引用。
软引用
被SoftReference
指向的对象可能会被垃圾收集器回收,但是只有在JVM内存不够的情况下才会回收;如下代码可以创建一个软引用:
Integer prime = 1;
SoftReference<Integer> soft = new SoftReference<Integer>(prime);
prime = null;
弱引用
当一个对象仅仅被WeakReference
引用时,在下个垃圾收集周期时候该对象就会被回收。我们通过下面代码创建一个WeakReference:
Integer prime = 1;
WeakReference<Integer> soft = new WeakReference<Integer>(prime);
prime = null;
当把prime赋值为null的时候,原prime对象会在下一个垃圾收集周期中被回收,因为已经没有强引用指向它。