1. 程式人生 > >java中的強,軟,弱,虛引用(及利用軟引用實現快取記憶體)

java中的強,軟,弱,虛引用(及利用軟引用實現快取記憶體)

在java中引用的型別一共有四種,分別是:強引用,軟引用,弱引用和虛引用。
那麼他們各自的定義是什麼呢?
1.強引用(StrongReference)
強引用是使用最普通的應用。如果一個物件具有強引用,那麼gc絕不會回收它。當記憶體空間不足,java虛擬機器寧願丟擲OOM(OutOfMemory),使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。
2.軟引用(SoftReference):
如果一個物件只具有軟引用,則記憶體空間足夠,gc就不會回收它。如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可以用來實現記憶體敏感的快取記憶體。(下有例子)。
軟引用常用的用法:軟引用可以和一個應用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。
3.弱引用(WeakReference)


弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在gc執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間是否充足,都會回收它的記憶體。不過,由於gc是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。
弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。這點跟軟引用的常用用法沒有什麼不同。
4.虛引用(PhantomReference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤物件被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之 關聯的引用佇列中。

ReferenceQueue queue = new ReferenceQueue();
    PhantomReference pr = new PhantomReference(new Object(),queue);

程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。
使用軟引用來實現快取記憶體
需要使用軟引用的應用場景
我們將使用一個Java語言實現的僱員資訊查詢系統查詢儲存在磁碟檔案或者資料庫中的僱員人事檔案資訊。作為一個使用者,我們完全有可能需要回頭去檢視幾分鐘甚至幾秒鐘前檢視過的僱員檔案資訊(同樣,我們在瀏覽WEB頁面的時候也經常會使用“後退”按鈕)。這時我們通常會有兩種程式實現方式:一種是把過去檢視過的僱員資訊儲存在記憶體中,每一個儲存了僱員檔案資訊的Java物件的生命週期貫穿整個應用程式始終;另一種是當用戶開始檢視其他僱員的檔案資訊的時候,把儲存了當前所檢視的僱員檔案資訊的Java物件結束引用,使得垃圾收集執行緒可以回收其所佔用的記憶體空間,當用戶再次需要瀏覽該僱員的檔案資訊的時候,重新構建該僱員的資訊。很顯然,第一種實現方法將造成大量的記憶體浪費,而第二種實現的缺陷在於即使垃圾收集執行緒還沒有進行垃圾收集,包含僱員檔案資訊的物件仍然完好地儲存在記憶體中,應用程式也要重新構建一個物件。我們知道,訪問磁碟檔案、訪問網路資源、查詢資料庫等操作都是影響應用程式執行效能的重要因素,如果能重新獲取那些尚未被回收的Java物件的引用,必將減少不必要的訪問,大大提高程式的執行速度。

我們通過一個僱員資訊查詢系統的小例子來說明如何構建一種快取記憶體器來避免重複構建同一個物件帶來的效能損失。我們將一個僱員的檔案資訊定義為一個Employee類:

package com.mydoctest;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

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() {
        //通過sleep來模擬從資料庫或者檔案中查資訊的耗時操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("sleep error");
            e.printStackTrace();
        }
    }
    public String getID(){
        return id;
    }

}

這個Employee類的構造方法中我們可以預見,如果每次需要查詢一個僱員的資訊。哪怕是幾秒鐘之前查詢過的,都要重新構建一個例項,這是需要消耗很多時間的。下面是一個對Employee物件進行快取的快取器的定義:

package com.mydoctest;

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;//用於cache內容的儲存
    private ReferenceQueue<Employee> q;//垃圾reference的佇列

    //繼承softreference,使得每一個例項都具有可識別的標識
    //並且該標識與其在hashmap內的key相同

    private class EmployeeRef extends SoftReference<Employee>{
        private String _key = "";

        public EmployeeRef(Employee em, ReferenceQueue<? super 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 = employeeRefs.get(ID);
            em = 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();
    }
}




我的測試程式碼為:

package test3;

public class Main {

    public static void main(String[] args) {
        EmployeeCache cache = EmployeeCache.getInstance();
        System.out.println("start");
        System.out.println("k1 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k1 end:"+System.currentTimeMillis());
        System.out.println("k2 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k2 end:"+System.currentTimeMillis());
        System.out.println("k3 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k3 end:"+System.currentTimeMillis());
        System.out.println("k4 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k4 end:"+System.currentTimeMillis());
        cache.clearCache();
        System.out.println("after clear");
        System.out.println("k5 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("1").getID());
        System.out.println("k5 end:"+System.currentTimeMillis());
        System.out.println("k6 start:"+System.currentTimeMillis());
        System.out.println(cache.getEmployee("2").getID());
        System.out.println("k6 end:"+System.currentTimeMillis());
    }

}

下面是執行結果:

start
k1 start:1453194918981
retrieve from employeeinfocenter.id1
1
k1 end:1453194921982
k2 start:1453194921982
retrieve from employeeinfocenter.id2
2
k2 end:1453194924982
k3 start:1453194924982
1
k3 end:1453194924982
k4 start:1453194924982
2
k4 end:1453194924983
after clear
k5 start:1453194925010
retrieve from employeeinfocenter.id1
1
k5 end:1453194928010
k6 start:1453194928010
retrieve from employeeinfocenter.id2
2
k6 end:1453194931010

證明確實能夠快取,並且節省了時間。
另外補充相關的物件可及性的知識。
物件可及性的判斷
在很多時候,一個物件並不是從根集直接引用的,而是一個物件被其他物件引用,甚至被幾個物件所引用,從而構成一個以根集為頂的樹形結構。如圖:
這裡寫圖片描述

在這個樹形的引用鏈中,箭頭的方向代表了引用的方向,所指向的物件是被引用物件。由圖可以看出,從根集到一個物件可以由很多條路徑。比如到達物件5的路徑就有①-⑤,③-⑦兩條路徑。由此帶來了一個問題,那就是某個物件的可及性如何判斷:
◆單條引用路徑可及性判斷:在這條路徑中,最弱的一個引用決定物件的可及性。
◆多條引用路徑可及性判斷:幾條路徑中,最強的一條的引用決定物件的可及性。
比如,我們假設圖2中引用①和③為強引用,⑤為軟引用,⑦為弱引用,對於物件5按照這兩個判斷原則,路徑①-⑤取最弱的引用⑤,因此該路徑對物件5的引用為軟引用。同樣,③-⑦為弱引用。在這兩條路徑之間取最強的引用,於是物件5是一個軟可及物件。