理解Java-Reference

引用類型

JDK1.2之后,Java擴充了引用的概念,將引用分為強引用、軟引用、弱引用和虛引用四種。

  • 強引用

    類似于"Object a = new Object()"這類的引用,只要垃圾強引用存在,垃圾回收器就不會回收掉被引用的對象。

  • 軟引用

    對于軟引用關聯的對象,在系統將要發生內存溢出異常之前,會把這些對象列入垃圾回收范圍中進行回收。如果這次回收還沒有足夠內存,則拋出內存異常。

    使用SoftReference類實現軟引用

  • 弱引用

    強度比軟引用更弱,被弱引用關聯的對象只能存活到下一次垃圾回收發生之前。當發生GC時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

    使用WeakReference類實現弱引用

  • 虛引用

    一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能夠在這個對象被垃圾回收器回收掉后收到一個通知。

    使用PhantomReference類實現虛引用

使用場景

強引用代碼中隨處可見,對于其他幾種引用則不太熟悉,他們有什么作用呢?

假設有這樣一個需求:每次創建一個數據庫Connection的時候,需要將用戶信息User與之關聯。典型的用法就是在一個全局的Map中存儲Connection和User的映射。

public class ConnManager {
    private Map<Connection,User> m = new HashMap<Connection,User>();

    public void setUser(Connection s, User u) {
        m.put(s, u);
    }
    public User getUser(Connection s) {
        return m.get(s);
    }
    public void removeUser(Connection s) {
        m.remove(s);
    }
}

這種方法的問題是User的生命周期與Connection掛鉤,我們無法準確預支Connection在什么時候結束,所以需要在每個Connection關閉之后,手動從Map中移除鍵值對,否則Connection和User將一直被Map引用,即使Connection的生命周期已經結束了,GC也無法回收對應的Connection和User。這些對象留在內存中不受控制,可能會造成內存溢出。

那么,如何避免手動的從Map中刪除對象呢?

利用 WeakHashMap 即可實現:

public class ConnManager {
    private Map<Connection,User> m = new WeakHashMap<Connection,User>();

    public void setUser(Connection s, User u) {
        m.put(s, u);
    }
    public User getUser(Connection s) {
        return m.get(s);
    }
}

WeakHashMap 與 HashMap類似,但是在其內部,key是經過WeakReference包裝的。使用WeakHashMap情況會變得怎樣呢?

每當垃圾回收發生時,那些已經結束生命周期的Connection對象(沒有強引用指向它)不受WeakHashMap中key(WeakReference)的影響,可以直接回收掉。同時,WeakHashMap利用ReferenceQueue(下文會提到) 可以做到刪除那些已經被回收的Connection對應的User。是不是做到了內存的自動管理呢?

可達性分析算法

Java執行GC時,需要判斷對象是否存活。判斷一個對象是否存活使用了"可達性分析算法"。

基本思路就是通過一系列稱為GC Roots的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,即從GC Roots到這個對象不可達時,證明此對象不可用。

image.png

可以作為GC Roots的對象包括:

  • 虛擬機棧中引用的對象

  • 方法區中類靜態屬性引用的對象

  • 方法區中常量引用的對象

  • 本地方法棧JNI引用的對象

往往到達一個對象的引用鏈會存在多條,垃圾回收時會依據兩個原則來判斷對象的可達性:

  • 單一路徑中,以最弱的引用為準

  • 多路徑中,以最強的引用為準

image.png

Reference && ReferenceQueue

SoftReference,WeakReference,PhantomReference擁有共同的父類Reference,看一下其內部實現:

Reference的構造函數最多可以接受兩個參數:Reference(T referent, ReferenceQueue<? super T> queue)

referent:即Reference所包裝的引用對象

queue:此Reference需要注冊到的引用隊列

ReferenceQueue本身提供隊列的功能,ReferenceQueue對象同時保存了一個Reference類型的head節點,Reference封裝了next字段,這樣就是可以組成一個單向鏈表。

ReferenceQueue主要用來確認Reference的狀態。Reference對象有四種狀態:

  • active

    GC會特殊對待此狀態的引用,一旦被引用的對象的可達性發生變化(如失去強引用,只剩弱引用,可以被回收),GC會將引用放入pending隊列并將其狀態改為pending狀態

  • pending

    位于pending隊列,等待ReferenceHandler線程將引用入隊queue

  • enqueue

    ReferenceHandler將引用入隊queue

  • inactive

    引用從queue出隊后的最終狀態,該狀態不可變

Reference與ReferenceQueue之間是如何工作的呢?

Reference里有個靜態字段pending,同時還通過靜態代碼塊啟動了Reference-handler thread。當一個Reference的referent被回收時,垃圾回收器會把reference添加到pending這個鏈表里,然后Reference-handler thread不斷的讀取pending中的reference,把它加入到對應的ReferenceQueue中。

當reference與referenQueue聯合使用的主要作用就是當reference指向的referent回收時,提供一種通知機制,通過queue取到這些reference,來做額外的處理工作。

PhantomReference的一個使用案例

上文提到 當reference與referenQueue聯合使用的主要作用就是當reference指向的referent回收時,提供一種通知機制,通過queue取到這些reference,來做額外的處理工作。通過PhantomReference的一個例子來加深體會:用PhantomReference來自動關閉文件流

使用PhantomReference封裝引用

public class ResourcePhantomReference<T> extends PhantomReference<T> {

    private List<Closeable> closeables;

    public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) {
        super(referent, q);
        closeables = resource;
    }

    public void cleanUp() {
        if (closeables == null || closeables.size() == 0)
            return;
        for (Closeable closeable : closeables) {
            try {
                closeable.close();
                System.out.println("clean up:"+closeable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

守護者線程利用ReferenceQueue做自動清理

public class ResourceCloseDeamon extends Thread {

    private static ReferenceQueue QUEUE = new ReferenceQueue();

    //保持對reference的引用,防止reference本身被回收
    private static List<Reference> references=new ArrayList<>();
    @Override
    public void run() {
        this.setName("ResourceCloseDeamon");
        while (true) {
            try {
                ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
                reference.cleanUp();
                references.remove(reference);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void register(Object referent, List<Closeable> closeables) {
        references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
    }
}

封裝的文件操作

public class FileOperation {

    private FileOutputStream outputStream;

    private FileInputStream inputStream;

    public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
        this.outputStream = outputStream;
        this.inputStream = inputStream;
    }

    public void operate() {
        try {
            inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

測試

ublic class PhantomTest {

    public static void main(String[] args) throws Exception {
        //打開回收
        ResourceCloseDeamon deamon = new ResourceCloseDeamon();
        deamon.setDaemon(true);
        deamon.start();

        // touch a.txt b.txt
        // echo "hello" > a.txt

        //保留對象,防止gc把stream回收掉,其不到演示效果
        List<Closeable> all=new ArrayList<>();
        FileInputStream inputStream;
        FileOutputStream outputStream;

        for (int i = 0; i < 100000; i++) {
            inputStream = new FileInputStream("/Users/robin/a.txt");
            outputStream = new FileOutputStream("/Users/robin/b.txt");
            FileOperation operation = new FileOperation(inputStream, outputStream);
            operation.operate();
            TimeUnit.MILLISECONDS.sleep(100);

            List<Closeable>closeables=new ArrayList<>();
            closeables.add(inputStream);
            closeables.add(outputStream);
            all.addAll(closeables);
            ResourceCloseDeamon.register(operation,closeables);
            //用下面命令查看文件句柄,如果把上面register注釋掉,就會發現句柄數量不斷上升
            //jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs  lsof -p  | grep /User/robin
            System.gc();

        }
    }
}

參考自Java Reference詳解

WeakHashMap

WeakHashMap實現原理很簡單,它除了實現標準的Map接口,里面的機制也和HashMap的實現類似。從它entry子類中可以看出,它的key是用WeakReference封裝的。

WeakHashMap里聲明了一個queue,Entry繼承WeakReference,構造函數中用key和queue關聯構造一個weakReference。當key所封裝的對象被GC回收后,GC自動將key注冊到queue中。

WeakHashMap中有代碼檢測這個queue,取出其中的元素,找到WeakHashMap中相應的鍵值對進行remove。這部分代碼就是expungeStaleEntries方法:

private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, 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;
                    // Must not null out e.next;
                    // stale entries may be in use by a HashIterator
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

這段代碼會在resize,getTable,size里執行,清除失效的entry。

FinalReference

在Reference的子類中,還有一個名為FinalReference的類,這個類用來做什么呢?

FinalReference僅僅繼承了Reference,沒有做其他的邏輯,只是將訪問權限聲明為package,所以我們不能夠直接使用它。

需要關注的是其子類 Finalizer,看一下他的實現:

首先,哪些類對象是Finalizer reference類型的referent呢? 只要類覆寫了Object 上的finalize方法,方法體非空。那么這個類的實例都會被Finalizer引用類型引用。這個工作是由虛擬機完成的,對于我們來說是透明的。

Finalizer 中有兩個字段需要關注:

queue:private static ReferenceQueue queue = new ReferenceQueue() 即上文提到的ReferenceQueue,用來實現通知

unfinalized:private static Finalizer unfinalized 維護了一個未執行finalize方法的reference列表。維護靜態字段unfinalized的目的是為了一直保持對未未執行finalize方法的reference的強引用,防止被gc回收掉。

在Finalizer的構造函數中通過add()方法把Finalizer引用本身加入到unfinalized列表中,同時關聯finalizee和queue,實現通知機制。

Finalizer靜態代碼塊里啟動了一個deamon線程 FinalizerThread,FinalizerThread run方法不斷的從queue中去取Finalizer類型的reference,然后調用Finalizer的runFinalizer方法,該方法最后執行了referent所重寫的finalize方法。

private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            jla.invokeFinalize(finalizee);
            /* Clear stack slot containing this variable, to decrease
               the chances of false retention with a conservative GC */
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

觀察上面的代碼,hasBeenFinalized()判斷了finalize是否已經執行,如果執行,則把這個referent從unfinalized隊列中移除。所以,任何一個對象的finalize方法只會被系統自動調用一次。當下一次GC發生時,由于unfinalized已經不再持有該對象的referent,故該對象被直接回收掉。

從上面的過程也可以看出,覆蓋了finalize方法的對象至少需要兩次GC才可能被回收。第一次GC把覆蓋了finalize方法的對象對應的Finalizer reference加入referenceQueue等待FinalizerThread來執行finalize方法。第二次GC才有可能釋放finalizee對象本身,前提是FinalizerThread已經執行完finalize方法了,并把Finalizer reference從Finalizer靜態unfinalized鏈表中剔除,因為這個鏈表和Finalizer reference對finalizee構成的是一個強引用。

參考

Java Reference詳解

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容