1.對象的強、軟、弱和虛引用
在JDK 1.2以前的版本中,若一個對象不被任何變量引用,那麼 程序就無法再使用這個對象。也就是說,只有對象處於可觸及 (reachable)狀態,程序才能使用它。從JDK 1.2版本開始,把對 象的引用分為4種級別,從而使程序能更加靈活地控制對象的生命 周期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛 引用。圖1為對象應用類層次。
圖1
⑴強引用(StrongReference)
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃 圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出 OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有 強引用的對象來解決內存不足的問題。
⑵軟引用(SoftReference)
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就 不會回收它;如果內存空間不足了,就會回收這些對象的內存。只 要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用 來實現內存敏感的高速緩存(下文給出示例)。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如 果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個 軟引用加入到與之關聯的引用隊列中。
⑶弱引用(WeakReference)
弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫 的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中 ,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否, 都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線 程,因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如 果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用 加入到與之關聯的引用隊列中。
⑷虛引用(PhantomReference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同 ,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用 ,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器 回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與 軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器准備回收一個對象時 ,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛 引用加入到與之 關聯的引用隊列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解 被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經 被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前 采取必要的行動。
2.對象可及性的判斷
在很多時候,一個對象並不是從根集直接引用的,而是一個對 象被其他對象引用,甚至同時被幾個對象所引用,從而構成一個以 根集為頂的樹形結構。如圖2所示
在這個樹形的引用鏈中,箭頭的方向代表了引用的方向,所指 向的對象是被引用對象。由圖可以看出,從根集到一個對象可以由 很多條路徑。比如到達對象5的路徑就有①-⑤,③-⑦兩條路徑。 由此帶來了一個問題,那就是某個對象的可及性如何判斷:
◆單條引用路徑可及性判斷:在這條路徑中,最弱的一個引用決 定對象的可及性。
◆多條引用路徑可及性判斷:幾條路徑中,最強的一條的引用決 定對象的可及性。
比如,我們假設圖2中引用①和③為強引用,⑤為軟引用,⑦為 弱引用,對於對象5按照這兩個判斷原則,路徑①-⑤取最弱的引用 ⑤,因此該路徑對對象5的引用為軟引用。同樣,③-⑦為弱引用。 在這兩條路徑之間取最強的引用,於是對象5是一個軟可及對象。
3.使用軟引用構建敏感數據的緩存
3.1 為什麼需要使用軟引用
首先,我們看一個雇員信息查詢系統的實例。我們將使用一個 Java語言實現的雇員信息查詢系統查詢存儲在磁盤文件或者數據庫 中的雇員人事檔案信息。作為一個用戶,我們完全有可能需要回頭 去查看幾分鐘甚至幾秒鐘前查看過的雇員檔案信息(同樣,我們在 浏覽WEB頁面的時候也經常會使用“後退”按鈕)。這時我們通常會 有兩種程序實現方式:一種是把過去查看過的雇員信息保存在內存 中,每一個存儲了雇員檔案信息的Java對象的生命周期貫穿整個應 用程序始終;另一種是當用戶開始查看其他雇員的檔案信息的時候 ,把存儲了當前所查看的雇員檔案信息的Java對象結束引用,使得 垃圾收集線程可以回收其所占用的內存空間,當用戶再次需要浏覽 該雇員的檔案信息的時候,重新構建該雇員的信息。很顯然,第一 種實現方法將造成大量的內存浪費,而第二種實現的缺陷在於即使 垃圾收集線程還沒有進行垃圾收集,包含雇員檔案信息的對象仍然 完好地保存在內存中,應用程序也要重新構建一個對象。我們知道 ,訪問磁盤文件、訪問網絡資源、查詢數據庫等操作都是影響應用 程序執行性能的重要因素,如果能重新獲取那些尚未被回收的Java 對象的引用,必將減少不必要的訪問,大大提高程序的運行速度。
3.2 如果使用軟引用
SoftReference的特點是它的一個實例保存對一個Java對象的軟 引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。 也就是說,一旦SoftReference保存了對一個Java對象的軟引用後 ,在垃圾線程對這個Java對象回收前,SoftReference類所提供的 get()方法返回Java對象的強引用。另外,一旦垃圾線程回收該 Java對象之後,get()方法將返回null。
看下面代碼:
MyObject aRef = new MyObject();
SoftReference aSoftRef=new SoftReference (aRef);
此時,對於這個MyObject對象,有兩個引用路徑,一個是來自 SoftReference對象的軟引用,一個來自變量aReference的強引用 ,所以這個MyObject對象是強可及對象。
隨即,我們可以結束aReference對這個MyObject實例的強引 用:
aRef = null;
此後,這個MyObject對象成為了軟可及對象。如果垃圾收集線 程進行內存垃圾收集,並不會因為有一個SoftReference對該對象 的引用而始終保留該對象。Java虛擬機的垃圾收集線程對軟可及對 象和其他一般Java對象進行了區別對待:軟可及對象的清理是由垃 圾收集線程根據其特定算法按照內存需求決定的。也就是說,垃圾 收集線程會在虛擬機拋出OutOfMemoryError之前回收軟可及對象, 而且虛擬機會盡可能優先回收長時間閒置不用的軟可及對象,對那 些剛剛構建的或剛剛使用過的“新”軟可反對象會被虛擬機盡可能 保留。在回收這些對象之前,我們可以通過:
MyObject anotherRef=(MyObject)aSoftRef.get ();
重新獲得對該實例的強引用。而回收之後,調用get()方法就只 能得到null了。
3.3 使用ReferenceQueue清除失去了軟引用對象的 SoftReference
作為一個Java對象,SoftReference對象除了具有保存軟引用的 特殊性之外,也具有Java對象的一般性。所以,當軟可及對象被回 收之後,雖然這個SoftReference對象的get()方法返回null,但這 個SoftReference對象已經不再具有存在的價值,需要一個適當的 清除機制,避免大量SoftReference對象帶來的內存洩漏。在 java.lang.ref包裡還提供了ReferenceQueue。如果在創建 SoftReference對象的時候,使用了一個ReferenceQueue對象作為 參數提供給SoftReference的構造方法,如:
ReferenceQueue queue = new ReferenceQueue();
SoftReference ref=new SoftReference(aMyObject, queue);
那麼當這個SoftReference所軟引用的aMyOhject被垃圾收集器 回收的同時,ref所強引用的SoftReference對象被列入 ReferenceQueue。也就是說,ReferenceQueue中保存的對象是 Reference對象,而且是已經失去了它所軟引用的對象的Reference 對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列 ,當我們調用它的poll()方法的時候,如果這個隊列中不是空隊列 ,那麼將返回隊列前面的那個Reference對象。
在任何時候,我們都可以調用ReferenceQueue的poll()方法來 檢查是否有它所關心的非強可及對象被回收。如果隊列為空,將返 回一個null,否則該方法返回隊列中前面的一個Reference對象。利 用這個方法,我們可以檢查哪個SoftReference所軟引用的對象已 經被回收。於是我們可以把這些失去所軟引用的對象的 SoftReference對象清除掉。常用的方式為:
SoftReference ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
// 清除ref
}
理解了ReferenceQueue的工作機制之後,我們就可以開始構造 一個Java對象的高速緩存器了。
3.4通過軟可及對象重獲方法實現Java對象的高速緩存
利用Java2平台垃圾收集機制的特性以及前述的垃圾對象重獲方 法,我們通過一個雇員信息查詢系統的小例子來說明如何構建一種 高速緩存器來避免重復構建同一個對象帶來的性能損失。我們將一 個雇員的檔案信息定義為一個Employee類:
public class Employee {
private String id;// 雇員的標識號碼
private String name;// 雇員姓名
private String department;// 該雇員所在部門
private String Phone;// 該雇員聯系電話
private int salary;// 該雇員薪資
private String origin;// 該雇員信息的來源
// 構造方法
public Employee(String id) {
this.id = id;
getDataFromlnfoCenter();
}
// 到數據庫中取得雇員信息
private void getDataFromlnfoCenter() {
// 和數據庫建立連接井查詢該雇員的信息,將查詢結果 賦值
// 給name,department,plone,salary等變量
// 同時將origin賦值為"From DataBase"
}
……
這個Employee類的構造方法中我們可以預見,如果每次需要查 詢一個雇員的信息。哪怕是幾秒中之前剛剛查詢過的,都要重新構 建一個實例,這是需要消耗很多時間的。下面是一個對Employee對 象進行緩存的緩存器的定義:
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Hashtable;
public class EmployeeCache {
static private EmployeeCache cache;// 一個Cache實例
private Hashtable<String,EmployeeRef> employeeRefs;// 用於Chche內容的存儲
private ReferenceQueue<Employee> q;// 垃圾 Reference的隊列
// 繼承SoftReference,使得每一個實例都具有可識別的 標識。
// 並且該標識與其在HashMap內的key相同。
private class EmployeeRef extends SoftReference<Employee> {
private String _key = "";
public EmployeeRef(Employee em, ReferenceQueue<Employee> q) {
super(em, q);
_key = em.getID();
}
}
// 構建一個緩存器實例
private EmployeeCache() {
employeeRefs = new Hashtable<String,EmployeeRef>();
q = new ReferenceQueue<Employee>();
}
// 取得緩存器實例
public static EmployeeCache getInstance() {
if (cache == null) {
cache = new EmployeeCache();
}
return cache;
}
// 以軟引用的方式對一個Employee對象的實例進行引用並 保存該引用
private void cacheEmployee(Employee em) {
cleanCache();// 清除垃圾引用
EmployeeRef ref = new EmployeeRef(em, q);
employeeRefs.put(em.getID(), ref);
}
// 依據所指定的ID號,重新獲取相應Employee對象的實例
public Employee getEmployee(String ID) {
Employee em = null;
// 緩存中是否有該Employee實例的軟引用,如果有,從 軟引用中取得。
if (employeeRefs.containsKey(ID)) {
EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
em = (Employee) ref.get();
}
// 如果沒有軟引用,或者從軟引用中得到的實例是null ,重新構建一個實例,
// 並保存對這個新建實例的軟引用
if (em == null) {
em = new Employee(ID);
System.out.println("Retrieve From EmployeeInfoCenter. ID=" + ID);
this.cacheEmployee(em);
}
return em;
}
// 清除那些所軟引用的Employee對象已經被回收的 EmployeeRef對象
private void cleanCache() {
EmployeeRef ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
employeeRefs.remove(ref._key);
}
}
// 清除Cache內的全部內容
public void clearCache() {
cleanCache();
employeeRefs.clear();
System.gc();
System.runFinalization();
}
}
4.使用弱引用構建非敏感數據的緩存
4.1全局 Map 造成的內存洩漏
無意識對象保留最常見的原因是使用Map將元數據與臨時對象( transient object)相關聯。假定一個對象具有中等生命周期,比 分配它的那個方法調用的生命周期長,但是比應用程序的生命周期 短,如客戶機的套接字連接。需要將一些元數據與這個套接字關聯 ,如生成連接的用戶的標識。在創建Socket時是不知道這些信息的 ,並且不能將數據添加到Socket對象上,因為不能控制 Socket 類 或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這 些信息,如下面的 SocketManager 類所示:使用一個全局 Map 將 元數據關聯到一個對象。
public class SocketManager {
private Map<Socket, User> m = new HashMap<Socket, User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s) {
m.remove(s);
}
}
這種方法的問題是元數據的生命周期需要與套接字的生命周期 掛鉤,但是除非准確地知道什麼時候程序不再需要這個套接字,並 記住從 Map 中刪除相應的映射,否則,Socket 和 User 對象將會 永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這 會阻止 Socket 和 User 對象被垃圾收集,即使應用程序不會再使 用它們。這些對象留下來不受控制,很容易造成程序在長時間運行 後內存爆滿。除了最簡單的情況,在幾乎所有情況下找出什麼時候 Socket 不再被程序使用是一件很煩人和容易出錯的任務,需要人 工對內存進行管理。
4.2如何使用WeakHashMap
在Java集合中有一種特殊的Map類型—WeakHashMap,在這種Map 中存放了鍵對象的弱引用,當一個鍵對象被垃圾回收器回收時,那 麼相應的值對象的引用會從Map中刪除。WeakHashMap能夠節約存儲 空間,可用來緩存那些非必須存在的數據。關於Map接口的一般用 法。
下面示例中MapCache類的main()方法創建了一個WeakHashMap對 象,它存放了一組Key對象的弱引用,此外main()方法還創建了一 個數組對象,它存放了部分Key對象的強引用。
import java.util.WeakHashMap;
class Element {
private String ident;
public Element(String id) {
ident = id;
}
public String toString() {
return ident;
}
public int hashCode() {
return ident.hashCode();
}
public boolean equals(Object obj) {
return obj instanceof Element && ident.equals(((Element) obj).ident);
}
protected void finalize(){
System.out.println("Finalizing "+getClass ().getSimpleName()+" "+ident);
}
}
class Key extends Element{
public Key(String id){
super(id);
}
}
class Value extends Element{
public Value (String id){
super(id);
}
}
public class CanonicalMapping {
public static void main(String[] args){
int size=1000;
Key[] keys=new Key[size];
WeakHashMap<Key,Value> map=new WeakHashMap<Key,Value>();
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;
map.put(k, v);
}
System.gc();
}
}
從打印結果可以看出,當執行System.gc()方法後,垃圾回收器 只會回收那些僅僅持有弱引用的Key對象。id可以被3整除的Key對 象持有強引用,因此不會被回收。
4.3用 WeakHashMap 堵住洩漏
在 SocketManager 中防止洩漏很容易,只要用 WeakHashMap 代替 HashMap 就行了。(這裡假定SocketManager不需要線程安全 )。當映射的生命周期必須與鍵的生命周期聯系在一起時,可以使 用這種方法。用WeakHashMap修復 SocketManager。
public class SocketManager {
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
}
4.4配合使用引用隊列
WeakHashMap 用弱引用承載映射鍵,這使得應用程序不再使用 鍵對象時它們可以被垃圾收集,get() 實現可以根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射 。但是這只是防止 Map 的內存消耗在應用程序的生命周期中不斷 增加所需要做的工作的一半,還需要做一些工作以便在鍵對象被收 集後從 Map 中刪除死項。否則,Map 會充滿對應於死鍵的項。雖 然這對於應用程序是不可見的,但是它仍然會造成應用程序耗盡內 存。
引用隊列是垃圾收集器向應用程序返回關於對象生命周期的信 息的主要方法。弱引用有個構造函數取引用隊列作為參數。如果用 關聯的引用隊列創建弱引用,在弱引用對象成為 GC 候選對象時, 這個引用對象就在引用清除後加入到引用隊列中(具體參考上文軟 引用示例)。
WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法 ,大多數 Map 操作中會調用它,它去掉引用隊列中所有失效的引 用,並刪除關聯的映射。
5.UML:使用關聯類指明特定形式的引用
關聯類能夠用來指明特定形式的引用,如弱(weak)、軟 (soft)或虛 (phantom)引用。
也可以如下的構造型方式。
本文出自 “子 孑” 博客,請務必保留此出處 http://zhangjunhd.blog.51cto.com/113473/53092