之前我們提到過 GC,但當 Java 中引用的對象越來越多,會導致內存空間不足,最終會產生錯誤 OutOfMemoryError,並讓應用程序終止。那為什么 GC 在此時不能多收集一些對象呢?這就和今天說的引用類型有關了。
首先,從 JDK1.2 開始,對象的引用被划分為4種級別,從而使程序能更加靈活地控制對象的生命周期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
強引用
強引用(Strong Reference)是使用最普遍的引用。如果一個對象具有強引用,那么它永遠不會被 GC。例如:
Object strongReference = new Object();
當內存空間不足時,JVM 寧願拋出OutOfMemoryError,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
如果強引用對象不使用時,需要弱化從而可以被 GC,例如ArrayList中的clear()方法:
/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
顯式地設置強引用對象為null,或讓其超出對象的生命周期范圍,則垃圾回收器認為該對象不存在引用,就會回收這個對象。具體什么時候收集這要取決於具體的垃圾回收器。
軟引用
如果一個對象只具有軟引用(Soft Reference),當內存空間充足時,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。讓我們來看一個例子具體了解一下:
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str);
String result = softReference.get();
讓我們來看一下get():
public T get() {
T o = super.get();
// timestamp代表上一次軟引用上一次被使用的時間(初始化、get())
// clock代表上一次GC的時間
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
因此,軟引用在被垃圾回收時,也遵循LRU法則,優先回收最近最少被使用的對象進行回收。
軟引用的使用場景多是內存敏感的高速緩存。具體來說,就是我們希望將數據存放到緩存中,這樣可以快速進行讀取。但是,當 JVM 中內存不夠用時,我們又不希望緩存數據會占用到 JVM 的內存。例如配合ReferenceQueue,如果軟引用所引用對象被垃圾回收,JVM 就會把這個軟引用加入到與之關聯的引用隊列中:
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null
但是需要注意的是,如果使用軟引用緩存,有可能導致Full GC增多。
弱引用
如果一個對象只具有弱引用(Weak Reference),其生命周期相比於軟引用更加短暫。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會對它進行回收。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。其使用為:
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = weakReference.get();
講到弱引用,就不得不提到WeakHashMap。和HashMap相比,當我們給 JVM 分配的內存不足的時候,HashMap 寧可拋出 OutOfMemoryError 異常,也不會回收其相應的沒有被引用的對象,而 WeakHashMap 則會回收存儲在其中但有被引用的對象。
WeakHashMap 通過將一些沒有被引用的鍵的值賦值為 null ,這樣的話就會告知GC去回收這些存儲的值了。假如我們特地傳入 key 為 null 的鍵,WeakHashMap 會將鍵設置為特殊的 Oject,源碼為:
public V put(K key, V value) {
// key會被重新賦值
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;
}
/**
* Value representing null keys inside tables.
* 特殊的key
*/
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;
}
虛引用
虛引用(PhantomReference),顧名思義,就是形同虛設。與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。 虛引用與軟引用和弱引用的一個區別在於:
虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。
例如:
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 創建虛引用,要求必須與一個引用隊列關聯
PhantomReference pr = new PhantomReference(str, queue);
程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要進行垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動,也可以理解為一種回調方法。
總結
Java 中4種引用的級別和強度由高到低依次為:強引用 -> 軟引用 -> 弱引用 -> 虛引用
通過表格,說明其特性:
| 引用類型 | 被垃圾回收的時間 | 使用場景 | 生存時間 |
|---|---|---|---|
| 強引用 | 從來不會 | 對象的一般狀態 | JVM停止運行時 |
| 軟引用 | 內存不足時 | 對象緩存 | 內存不足時 |
| 弱引用 | 正常垃圾回收時 | 對象緩存 | 垃圾回收后終止 |
| 虛引用 | 正常垃圾回收時 | 跟蹤對象的垃圾回收 | 垃圾回收后終止 |
有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。


