WeakHashMap其实也是java不常见的东西,但是和linkedHashMap一样,有它自己独特的功能。在本篇博文中我会用例子详细介绍它独有的属性,同时会对照源码来解释为什么它具备这样的功能。在知识点中会扩展关于引用的相关知识,帮助后面的理解。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290
1、java中的引用
关于java中的引用,其实我在“GC-谈谈“生死””这篇文章中就详细介绍过引用的概念,从原来的粗犷定义到现在的定义(http://blog.csdn.net/u012403290/article/details/65698856)。引用类型主要分为4种:①强引用;②软引用;③弱引用;④虚引用。强引用就是永远不会回收掉被引用的对象,比如说我们代码中new出来的对象。软引用表示有用但是非必需的,如果系统内存资源紧张,可能就会被回收;弱引用表示非必需的对象,只能存活到下一次垃圾回收发生之前;虚引用是最弱的,这个引用无法操作对象。在java中有与之对应的对象:SoftReference(软引用), WeakReference(弱引用),PhantomReference(虚引用)。在我们今天要研究的WeakHashMap中用WeakReference来实现。
2、引用队列ReferenceQueue
根据本人的理解,引用队列就相当于一个电话簿一样的东西,用于监听和管理在引用对象中被回收的对象。具体我用一段代码来解释:
Object o1 = new Object();
Integer o2 = new Integer((int) o1);
比如说上面两段代码,在我们看来如果o2对象不被回收的话,o1永远都不可能被回收。但是在引用(Reference)中,存在这么一个情况:如果o1对象除了在o2中有引用之外没有别的地方存在引用,那么就可以回收o1。然后当这个o1被回收之后,我们就需要把o2放入引用队列中,所以引用队列(ReferenceQueue)就是Reference的监听器。在WeakHashMap中就是通过ReferenceQueue来反向处理map中的数据,如果对象被回收了,那么就需要把map中的对应数据移除。
或许对于上面的介绍,很多人都看不懂的,接下来我先用HashMap建立一个例子帮大家理解:
package com.brickworkers;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class ReferenceTest {
private static final int _1MB = 1024*1024;//设置大小为1MB
public static void main(String[] args) throws InterruptedException {
ReferenceQueue
注意,回收的对象是bytes,并不是weakReference, 这也是为什么HashMap中的数据长度并没有发生变化的原因。我们再梳理一下运行流程:
1、bytes对象存入到weakReference对象中。
2、weakReference对象作为key,一个Object作为值存入HashMap中
2、GC回收了bytes对象,这个时候就要把引用这个对象的weakReference对象存储到ReferenceQueue中
3、死循环ReferenceQueue, 打印出被回收的对象。
下面就是上来的引用关系:
HashMap ——>weakReference——>byte[],千万注意被回收的是byte[]对象。
其实,上面这个逻辑就是核心WeakHashMap的实现,WeakHashMap只不过比上述的代码多了一步:把引用回收的对象从Map中移除罢了。
package com.brickworkers;
import java.util.Map;
import java.util.WeakHashMap;
public class ReferenceTest {
private static final int _1MB = 1024*1024;//设置大小为1MB
public static void main(String[] args) throws InterruptedException {
Object value = new Object();
Map map = new WeakHashMap();
for (int i = 0; i < 100; i++) {//循环100次把数据插入WeakHashMap中
byte[] bytes = new byte[_1MB];
map.put(bytes, value);
}
while (true) {//死循环监控map大小变化
Thread.sleep(500);//稍稍停顿,效果更直观
System.out.println(map.size());//打印WeakHashMap的大小
System.gc();//建议系统进行GC
}
}
//截取一部分输出:
// 41
// 0
// 0
// 0
}
以上的代码就是用WeakHashMap来实现了,你会说为什么不直接在最上面的代码把HashMap改成WeakHashMap就行了呢?不行的!WeakHashMap在类的内部就构建了引用队列(ReferenceQueue)和弱引用(weakReference ),具体的下面源码会介绍到。我们先来分析和解释一下上面的代码,一般人会有2个疑问:
①为什么我插入100个数据,第一次打印是41呢?
因为在插入的过程中已经触发过GC了,你可以把size的打印放到循环内部,你就会发现原因。同时为什么是到达41呢?这个和你的内存有关系,如果内存很富足,它就不会发生GC,而且弱引用是鸡肋一般的东西:食之无味,弃之可惜。他们只能存活到下次GC之前。
②为什么后来又变成0了呢?
因为在打印了大小之后,我建议系统(System.gc())发起一次GC操作,为什么说是建议呢?因为系统不一定会接收到你 指令就会发生GC的。一旦GC发生,那么弱引用就会被清除,导致WeakHashMap的大小为0。
同时,值得一提的是,存在WeakHashMap中的数据,并不会平白无故就给你移除了map中的数据,必然是你触发了一些操作,在上述代码中size方法就会触发这个操作,下面是size的源码:
/**
* Returns the number of key-value mappings in this map.
* This result is a snapshot, and may not reflect unprocessed
* entries that will be removed before next attempted access
* because they are no longer referenced.
*/
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();//这个操作就是处理到不存在的引用方法
return size;
}
当然不仅仅在size方法会触发,下面源码介绍我们会讲到。
当然,理解Map相关的,需要你对Map有所了解,如果你不是很了解请参考一下我写的关于HashMap的博文:http://blog.csdn.net/u012403290/article/details/65442646。
1、在WeakHashMap中核心成员变量(关于HashMap中已存在的不再赘述):
①引用队列
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue queue = new ReferenceQueue<>();
这个队列其实就是我们前面研究过的,用于监控对象回收的情况。
②静态内部类Entry K,V
下面是我截取的部分源码
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
//继承了弱引用WeakReference, 同时实现了Map.Entry接口
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue queue,//这里把引用队列进行关联
int hash, Entry next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
.
.
.//这里还重写了一些方法
}
这个静态内部类也是WeakHashMap的核心,因为它把key值封装进了弱引用(WeakReference)中,这样一来,就回到了我们最前面的例子中,GC的时候可以回收掉弱引用对象中引用的对象(很拗口是不是?我自己写的自己读都拗口,其实就是真正的key值被弱引用WeakReference包装了),在源码中super实现了弱引用与引用队列关联的构造器,这样引用队列可以对弱引用进行监控了。
③核心移除map中K,V方法
如此一来,结合最前面的代码,我们对WeakHashMap的理解已经基本成型了。接下来,我们要解释一下为什么对象回收之后,map中的对应K,V也会被移除,核心就是下面这个方法:
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {//存在对象被GC, 那么就需要移除map中对应的数据
synchronized (queue) {//线程同步,锁定队列
@SuppressWarnings("unchecked")
Entry e = (Entry) x;
int i = indexFor(e.hash, table.length);//定位到节点位置
Entry prev = table[i];
Entry p = prev;
while (p != null) {//如果P节点存在
Entry next = p.next;//定义一个next节点指向p的下个节点
if (p == e) {//如果P就是当前节点
if (prev == e)
table[i] = next;//意思就是桶中第一个数据就是需要移除的,直接把第二个节点放到头节点的位置
else
prev.next = next;//那就把上个节点的下个节点指向p后面的节点
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC帮助GC,直接删除e的对应value值
size--;//减少WeakHashMap的大小
break;//结束
}
prev = p;
p = next;
}
}
}
}
或许有小伙伴看源代码有些吃力,我把上面这段代码主要做的事情写出来:
①:循环遍历引用队列(queue), 如果发现某个对象被GC了,那么就开始处理。
②:如果被处理的这个节点是头节点,那么直接把该节点的下个节点放到头节点,然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。
③:如果被处理的这个节点不是头结点,那么就需要把这个节点的上个节点中的next指针直接指向当前节点的下个节点。意思就是a->b->c,这个时候要移除b,那么就变成a->c。然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。
那么在那些时候出发这个expungeStaleEntries方法呢?查询源码之后就会发现好多方法都会调用这个方法:
//size方法
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();//去除被回收的对象
return size;
}
//getTable方法(这个方法是put和get方法的辅助方法)
/**
* Returns the table after first expunging stale entries.
*/
private Entry[] getTable() {
expungeStaleEntries();//去除被回收的对象
return table;
}
//resize扩容方法
void resize(int newCapacity) {
Entry[] oldTable = getTable();
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] 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.
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();//去除被回收的对象
transfer(newTable, oldTable);
table = oldTable;
}
}
//get方法(基于上面说的getTable方法)
public V get(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry[] tab = getTable();//getTable中包装了expungeStaleEntries方法
int index = indexFor(h, tab.length);
Entry e = tab[index];
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
//put方法(基于上面说的getTable方法)
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry[] tab = getTable();//getTable中包装了expungeStaleEntries方法
int i = indexFor(h, tab.length);
for (Entry 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;
}
}
这个方法是渗透在很多方法里面的,这里就不继续一一列举了,同时关于WeakHashMap的添加(put),获取(get), 扩容(resize)这里就不一一介绍了,如果不清楚的,请去查看我写的HashMap详解,里面我又详细介绍过。
如果你觉得我那里说的不对,或者有更好的解释,欢迎留言交流。我说的并不一定对,可能只是一个简单的指导作用,大家可以自己深入研究一下,或许会有别开新面的收获哦。