在JDK1.2以前的版本中,當一個對象不被任何變數引用,那麼程式就無法再使用這個對象。也就是說,只有對象處於可觸及狀態,程式才能使用它。這 就像在日常生活中,從商店購買了某樣物品後,如果有用,就一直保留它,否則就把它扔到垃圾箱,由清潔工人收走。一般說來,如果物品已經被扔到垃圾箱,想再 把它撿回來使用就不可能了。
但有時候情況並不這麼簡單,你可能會遇到類似雞肋一樣的物品,食之無味,棄之可惜。這種物品現在已經無用了,保留它會佔空間,但是立刻扔掉它也不划算,因 為也許將來還會派用場。對於這樣的可有可無的物品,一種折衷的處理辦法是:如果家裡空間足夠,就先把它保留在家裡,如果家裡空間不夠,即使把家裡所有的垃 圾清除,還是無法容納那些必不可少的生活用品,那麼再扔掉這些可有可無的物品。
從JDK1.2版本開始,把對象的引用分為四種層級,從而使程式能更加靈活的控制對象的生命週期。這四種層級由高到低依次為:強引用、軟引用、弱引用和虛引用。
1.強引用
本章前文介紹的引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,記憶體回收行程絕不會回收它。當記憶體空 間不足,Java虛擬機器寧願拋出OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的對象來解決記憶體不足問題。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似於可有可物的生活用品。如果記憶體空間足夠,記憶體回收行程就不會回收它,如果記憶體空間不足了,就會回收這些對象的記憶體。只要記憶體回收行程沒有回收它,該對象就可以被程式使用。軟引用可用來實現記憶體敏感的快取。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被記憶體回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似於可有可物的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在記憶體回收行程線程掃描它 所管轄的記憶體地區的過程中,一旦發現了只具有弱引用的對象,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於記憶體回收行程是一個優先順序很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被記憶體回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用隊列中。
4.虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被記憶體回收。
虛引用主要用來跟蹤對象被記憶體回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是 否已經加入了虛引用,來瞭解
被引用的對象是否將要被記憶體回收。程式如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前採取必要的行動。
在本書中,"引用"既可以作為動詞,也可以作為名詞,讀者應該根據上下文來區分"引用"的含義。
在java.lang.ref包中提供了三個類:SoftReference類、WeakReference類和PhantomReference類,它 們分別代表軟引用、弱引用和虛引用。ReferenceQueue類表示引用隊列,它可以和這三種引用類聯合使用,以便跟蹤Java虛擬機器回收所引用的對 象的活動。以下程式建立了一個String對象、ReferenceQueue對象和WeakReference對象:
//建立一個強引用
String str = new String("hello");
//建立引用隊列, 為範型標記,表明隊列中存放String對象的引用
ReferenceQueue rq = new ReferenceQueue();
//建立一個弱引用,它引用"hello"對象,並且與rq引用隊列關聯
//為範型標記,表明WeakReference會弱引用String對象
WeakReference wf = new WeakReference(str, rq);
以上程式碼執行完畢,記憶體中引用與對象的關係11-10所示。
圖11-10 "hello"對象同時具有強引用和弱引用
在圖11-10中,帶實線的箭頭表示強引用,帶虛線的箭頭表示弱引用。可以看出,此時"hello"對象被str強引用,並且被一個WeakReference對象弱引用,因此"hello"對象不會被記憶體回收。
在以下程式碼中,把引用"hello"對象的str變數置為null,然後再通過WeakReference弱引用的get()方法獲得"hello"對象的引用:
String str = new String("hello"); //①
ReferenceQueue rq = new ReferenceQueue(); //②
WeakReference wf = new WeakReference(str, rq); //③
str=null; //④取消"hello"對象的強引用
String str1=wf.get(); //⑤假如"hello"對象沒有被回收,str1引用"hello"對象
//假如"hello"對象沒有被回收,rq.poll()返回null
Reference ref=rq.poll(); //⑥
執行完以上第④行後,記憶體中引用與對象的關係11-11所示,此 時"hello"對象僅僅具有弱引用,因此它有可能被記憶體回收。假如它還沒有被記憶體回收,那麼接下來在第⑤行執行wf.get()方法會返回 "hello"對象的引用,並且使得這個對象被str1強引用。再接下來在第⑥行執行rq.poll()方法會返回null,因為此時引用隊列中沒有任何 引用。ReferenceQueue的poll()方法用於返回隊列中的引用,如果沒有則返回null。
圖11-11 "hello"對象只具有弱引用
在以下程式碼中,執行完第④行後,"hello"對象僅僅具有弱引用。接下來兩次調用System.gc()方法,催促記憶體回收行程工作,從而提高 "hello"對象被回收的可能性。假如"hello"對象被回收,那麼WeakReference對象的引用被加入到ReferenceQueue中, 接下來wf.get()方法返回null,並且rq.poll()方法返回WeakReference對象的引用。圖11-12顯示了執行完第⑧行後記憶體 中引用與對象的關係。
String str = new String("hello"); //①
ReferenceQueue rq = new ReferenceQueue(); //②
WeakReference wf = new WeakReference(str, rq); //③
str=null; //④
//兩次催促記憶體回收行程工作,提高"hello"對象被回收的可能性
System.gc(); //⑤
System.gc(); //⑥
String str1=wf.get(); //⑦ 假如"hello"對象被回收,str1為null
Reference ref=rq.poll(); //⑧
圖11-12 "hello"對象被記憶體回收,弱引用被加入到引用隊列
在以下常式11-15的References類中,依次建立了10個軟引用、10個弱引用和10個虛引用,它們各自引用一個Grocery對象。從程式運 行時的列印結果可以看出,虛引用形同虛設,它所引用的對象隨時可能被記憶體回收,具有弱引用的對象擁有稍微長的生命週期,當記憶體回收行程執行回收操作時,有可 能被記憶體回收,具有軟引用的對象擁有較長的生命週期,但在Java虛擬機器認為記憶體不足的情況下,也會被記憶體回收。
常式11-15 References.java
import java.lang.ref.*;
import java.util.*;
class Grocery{
private static final int SIZE = 10000;
//屬性d使得每個Grocery對象佔用較多記憶體,有80K左右
private double[] d = new double[SIZE];
private String id;
public Grocery(String id) { this.id = id; }
public String toString() { return id; }
public void finalize() {
System.out.println("Finalizing " + id);
}
}
public class References {
private static ReferenceQueue rq = new ReferenceQueue();
public static void checkQueue() {
Reference inq = rq.poll(); //從隊列中取出一個引用
if(inq != null)
System.out.println("In queue: "+inq+" : "+inq.get());
}
public static void main(String[] args) {
final int size=10;
//建立10個Grocery對象以及10個軟引用
Set> sa = new HashSet>();
for(int i = 0; i < size; i++) {
SoftReference ref=
new SoftReference(new Grocery("Soft " + i), rq);
System.out.println("Just created: " +ref.get());
sa.add(ref);
}
System.gc();
checkQueue();
//建立10個Grocery對象以及10個弱引用
Set> wa = new HashSet>();
for(int i = 0; i < size; i++) {
WeakReference ref=
new WeakReference(new Grocery("Weak " + i), rq);
System.out.println("Just created: " +ref.get());
wa.add(ref);
}
System.gc();
checkQueue();
//建立10個Grocery對象以及10個虛引用
Set> pa = new HashSet>();
for(int i = 0; i < size; i++) {
PhantomReferenceref =
new PhantomReference(new Grocery("Phantom " + i), rq);
System.out.println("Just created: " +ref.get());
pa.add(ref);
}
System.gc();
checkQueue();
}
}
在Java集合中有一種特殊的Map類型:WeakHashMap, 在這種Map中存放了鍵對象的弱引用,當一個鍵對象被記憶體回收,那麼相應的值對象的引用會從Map中刪除。WeakHashMap能夠節約儲存空間,可用 來緩衝那些非必須存在的資料。關於Map介面的一般用法,可參見本書第15章的15.4節(Map)。
以下常式11-16的MapCache類的main()方法建立了一個WeakHashMap對象,它存放了一組Key對象的弱引用,此外main()方法還建立了一個數組對象,它存放了部分Key對象的強引用。
常式11-16 MapCache.java
import java.util.*;
import java.lang.ref.*;
class Key {
String id;
public Key(String id) { this.id = id; }
public String toString() { return id; }
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object r) {
return (r instanceof Key)
&& id.equals(((Key)r).id);
}
public void finalize() {
System.out.println("Finalizing Key "+ id);
}
}
class Value {
String id;
public Value(String id) { this.id = id; }
public String toString() { return id; }
public void finalize() {
System.out.println("Finalizing Value "+id);
}
}
public class MapCache {
public static void main(String[] args) throws Exception{
int size = 1000;
// 或者從命令列獲得size的大小
if(args.length > 0)size = Integer.parseInt(args[0]);
Key[] keys = new Key[size]; //存放鍵對象的強引用
WeakHashMap whm = new WeakHashMap();
for(int i = 0; i < size; i++) {
Key k = new Key(Integer.toString(i));
Value v = new Value(Integer.toString(i));
if(i % 3 == 0) keys[i] = k; //使Key對象持有強引用
whm.put(k, v); //使Key對象持有弱引用
}
//催促記憶體回收行程工作
System.gc();
//把cpu讓給記憶體回收行程線程
Thread.sleep(8000);
}
}
以上程式的部分列印結果如下:
Finalizing Key 998
Finalizing Key 997
Finalizing Key 995
Finalizing Key 994
Finalizing Key 992
Finalizing Key 991
Finalizing Key 989
Finalizing Key 988
Finalizing Key 986
Finalizing Key 985
Finalizing Key 983
從列印結果可以看出,當執行System.gc()方法後,記憶體回收行程只會回收那些僅僅持有弱引用的Key對象。id可以被3整數的Key對象持有強引用,因此不會被回收。