总结这个类之前,首先看一下Java引用的相关知识。Java的引用分为四种:强引用、软引用、弱引用和虚引用。
强引用:就是常见的代码中的引用,如Object o = new Object();存在强引用的对象不会被垃圾收集器回收。所以我们会在一些代码中看到显示切断强引用,以便回收相关对象的情况。
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
软引用:比强引用弱一些,表示对一些有用但非必须对象的引用。这些对象会在将要发生内存溢出异常之前被回收。在Java中用java.lang.ref.SoftReference来表示软引用。看一个例子,先设置下JVM参数,将堆大小设为10m,老年代新生代各5m,打印出GC日志。
JVM参数
-Xms10m -Xmx10m -Xmn5m -XX:+PrintGCDetails
public static void main(String[] args) {
//构造一个3M的对象
Object _3m_1 = new byte[1024 * 1024 * 3];
//创建软引用
SoftReference<Object> reference = new SoftReference<Object>(_3m_1);
//切断强引用
_3m_1 = null;
//再构造一个3M的对象
Object _3m_2 = new byte[1024 * 1024 * 3];
//触发Full GC
System.gc();
System.out.println("软引用关联对象:"+reference.get());
}
输出结果:
[GC [DefNew: 3325K->152K(4608K), 0.0023022 secs] 3325K->3224K(9728K), 0.0023380 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [Tenured: 3072K->3072K(5120K), 0.0057404 secs] 6378K->6296K(9728K), [Perm : 380K->380K(12288K)], 0.0057837 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
软引用关联对象:[B@14318bb
Heap
def new generation total 4608K, used 3350K [0x31fe0000, 0x324e0000, 0x324e0000)
eden space 4096K, 81% used [0x31fe0000, 0x32325880, 0x323e0000)
from space 512K, 0% used [0x32460000, 0x32460000, 0x324e0000)
to space 512K, 0% used [0x323e0000, 0x323e0000, 0x32460000)
tenured generation total 5120K, used 3072K [0x324e0000, 0x329e0000, 0x329e0000)
the space 5120K, 60% used [0x324e0000, 0x327e0010, 0x327e0200, 0x329e0000)
compacting perm gen total 12288K, used 380K [0x329e0000, 0x335e0000, 0x369e0000)
基本过程是这样,程序先创建了一个3M的对象_3m_1,这个对象被分配到新生代里面,然后创了一个软引用关联到_3m_1,然后切断_3m_1的强引用。接下来又创建了一个3M的对象_3m_2,_3m_2要试图分配到新生代,这时发现新生代剩余空间不足(一共4608K,已经有一个3M的对象在里面)。接下来触发了一次新生代的垃圾收集动作(Minor GC),但这次收集没有回收掉_3m_1。然后_3m_1晋升到老年代,_3m_2被分配到新生代。接下来程序显示触发了一次Full GC,但这次Full GC似乎没起什么作用,_3m_1和_3m_2都安然无恙。
那么修改一下程序,再跑一次看看:
public static void main(String[] args) {
//构造一个3M的对象
Object _3m_1 = new byte[1024 * 1024 * 3];
//创建软引用
SoftReference<Object> reference = new SoftReference<Object>(_3m_1);
//切断强引用
_3m_1 = null;
//再构造一个3M的对象
Object _3m_2 = new byte[1024 * 1024 * 3];
//又构造一个3M的对象
Object _3m_3 = new byte[1024 * 1024 * 3];
System.out.println("软引用关联对象:"+reference.get());
}
输出结果:
[GC [DefNew: 3325K->152K(4608K), 0.0021553 secs] 3325K->3224K(9728K), 0.0021908 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 3224K->3224K(4608K), 0.0000288 secs][Tenured: 3072K->3072K(5120K), 0.0060759 secs] 6296K->6296K(9728K), [Perm : 380K->380K(12288K)], 0.0061594 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [Tenured: 3072K->3210K(5120K), 0.0049693 secs] 6296K->3210K(9728K), [Perm : 380K->375K(12288K)], 0.0050152 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
软引用关联对象:null
Heap
def new generation total 4608K, used 3280K [0x31fe0000, 0x324e0000, 0x324e0000)
eden space 4096K, 80% used [0x31fe0000, 0x32314050, 0x323e0000)
from space 512K, 0% used [0x32460000, 0x32460000, 0x324e0000)
to space 512K, 0% used [0x323e0000, 0x323e0000, 0x32460000)
tenured generation total 5120K, used 3210K [0x324e0000, 0x329e0000, 0x329e0000)
the space 5120K, 62% used [0x324e0000, 0x32802b28, 0x32802c00, 0x329e0000)
前面的过程和上面类似,区别在创建第三个3M对象_3m_3时,首先触发了一次Minor GC,没回收什么东西,但是要把新生代里的_3m_2放到老年代,这样才能腾出地方给_3m_3。但是老年代已经有_3m_1了,剩余的空间不足以放下_3m_2,这时系统触发了Full GC(不用我们触发system Full GC了)。从日志上看似乎回收掉了3M的空间。说明在内存溢出之前,软引用关联的对象被回收了。这便是软引用的特性,如果这里是一个强引用的话,那么便会出现OOM了。
弱引用:比软引用还弱一些,所以关联的对象在下一次垃圾收集的时候就会被回收掉。继续看例子,JVM参数同上。
public static void main(String[] args) {
//构造一个3M的对象
Object _3m_1 = new byte[1024 * 1024 * 3];
//创建软引用
WeakReference<Object> reference = new WeakReference<Object>(_3m_1);
//切断强引用
_3m_1 = null;
//触发Full GC
System.gc();
System.out.println("软引用关联对象:"+reference.get());
}
输出结果:
[Full GC (System) [Tenured: 0K->152K(5120K), 0.0086388 secs] 3325K->152K(9728K), [Perm : 380K->380K(12288K)], 0.0086922 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
软引用关联对象:null
Heap
def new generation total 4608K, used 208K [0x31fe0000, 0x324e0000, 0x324e0000)
eden space 4096K, 5% used [0x31fe0000, 0x32014040, 0x323e0000)
from space 512K, 0% used [0x323e0000, 0x323e0000, 0x32460000)
to space 512K, 0% used [0x32460000, 0x32460000, 0x324e0000)
tenured generation total 5120K, used 152K [0x324e0000, 0x329e0000, 0x329e0000)
the space 5120K, 2% used [0x324e0000, 0x32506088, 0x32506200, 0x329e0000)
虚引用:虚引用一般用来代替finalize方法来做一些引用对象被回收前的清理动作(由于finalize的不确定性,不建议使用)。虚引用必须配合一个ReferenceQueue一起使用,在GC决定要回收其引用对象时(引用对象没有任何强引用关联),虚引用会入栈,程序中可以在这个时机做一些清理工作。虚引用的get方法返回null:
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
这样保证了虚引用的关联对象永远不可能通过get方法再次获得强引用。但虚引用的关联对象要一直等到虚引用本身不可达或者被回收时才能够被回收,这点不同于软引用和弱引用。
最后说一下java.lang.ref.ReferenceQueue。ReferenceQueue是一个引用队列,构造一个软引用、弱引用或者虚引用可以传入一个ReferenceQueue,表示这个引用注册到传入的引用队列上,简单的说,当GC决定回收引用关联对象时,会将这个引用放到引用队列里。
大概了解了Java引用的相关内容,现在可以看下java.util.WeakHashMap的源码了。
整体看下来,其实和
HashMap差不多,所以只看一下差异的地方。
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<K> queue = new ReferenceQueue<K>();
WeakHashMap中多了一个引用队列的变量,大概能猜到是干什么的了吧。
看下WeakHashMap中的Entry:
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
private V value;
private final int hash;
private Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(K key, V value,
ReferenceQueue<K> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
public K getKey() {
return WeakHashMap.<K>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;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public int hashCode() {
Object k = getKey();
Object v = getValue();
return ((k==null ? 0 : k.hashCode()) ^
(v==null ? 0 : v.hashCode()));
}
public String toString() {
return getKey() + "=" + getValue();
}
}
java.util.WeakHashMap.Entry扩展自java.lang.ref.WeakReference,将Key(Map中的键)作为虚引用的关联对象。
由于WeakHashMap本身允许键值为null,所以这里不同于HashMap,需要利用一些转化函数。
/**
* Value representing null keys inside tables.
*/
private static final Object NULL_KEY = new Object();
/**
* Use NULL_KEY for key if it is null.
*/
private static Object maskNull(Object key) {
return (key == null ? NULL_KEY : key);
}
/**
* Returns internal representation of null key back to caller as null.
*/
private static <K> K unmaskNull(Object key) {
return (K) (key == NULL_KEY ? null : key);
}
由于虚引用的性质,当WeakHashMap中的某个Key已经没有外部的强引用,那么在接下来发生的垃圾收集动作里,它将会被回收。这里要注意一下,回收的仅仅是Key,但是Entry还在呢(也可以说是虚引用本身)。
所以在一些操作里会调用expungeStaleEntries方法,这个方法里会清理所有Key已经被回收的Entry。
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
Entry<K,V> e;
while ( (e = (Entry<K,V>) queue.poll()) != null) {
int h = e.hash;
int i = indexFor(h, 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;
e.next = null; // Help GC
e.value = null; // " "
size--;
break;
}
prev = p;
p = next;
}
}
}
/**
* Returns the table after first expunging stale entries.
*/
private Entry[] getTable() {
expungeStaleEntries();
return table;
}
/**
* 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;
}
基本上差异就是这些。WeakHashMap本身的特性也使它经常被应用到一些缓存的场景。