JVM成神之路-JVM引用模型

本文通過探析Java中的引用模型,分析比較強引用、軟引用、弱引用、虛引用的概念及使用場景,知其然且知其所以然,希望給大家在實際開發實踐、學習開源項目提供參考。

Java的引用

對於Java中的垃圾回收機制來說,對象是否被應該回收的取決於該對象是否被引用。因此,引用也是JVM進行內存管理的一個重要概念。Java中是JVM負責內存的分配和回收,這是它的優點(使用方便,程序不用再像使用C語言那樣擔心內存),但同時也是它的缺點(不夠靈活)。由此,Java提供了引用分級模型,可以定義Java對象重要性和優先級,提高JVM內存回收的執行效率

關於引用的定義,在JDK1.2之前,如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱爲這塊內存代表着一個引用;JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。

軟引用對象和弱應用對象主要用於:當內存空間還足夠,則能保存在內存之中;如果內存空間在垃圾收集之後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的使用場景。

而虛引用對象用於替代不靠譜的finalize方法,可以獲取對象的回收事件,來做資源清理工作。

對象生命週期

無分級引用對象生命週期

前面提到,分層引用的模型是用於內存回收,沒有分級引用對象下,一個對象從創建到回收的生命週期可以簡單地用下圖概括:對象被創建,被使用,有資格被收集,最終被收集,陰影區域表示對象“強可達”時間:

有分級引用對象生命週期

JDK1.2引入java.lang.ref程序包之後,對象的生命週期多了3個階段,軟可達,弱可達,虛可達,這些狀態僅適用於符合垃圾回收條件的對象,這些對象處於非強引用階段,而且需要基於java.lang.ref包中的相關的引用對象類來指示標明。

  • 軟可達
    軟可達對象用SoftReference來指示標明,並沒有強引用,垃圾回收器會盡可能長時間地保留對象,但是會在拋出OutOfMemoryError異常之前收集它。

  • 弱可達
    弱可達對象用WeakReference來指示標明,並沒有強引用或軟引用,垃圾回收器會隨時回收對象,並不會嘗試保留它,但是會在拋出OutOfMemoryError異常之前收集它。
    在對象回收階段中,該對象在major collection期間被回收,但是可以在minor collection期間存活

  • 虛可達 
    虛可達對象用PhantomReference來指示標明,它已經被標記選中進行垃圾回收並且它的finalizer(如果有)已經運行。在這種情況下,術語“可達”實際上是用詞不當,因爲您無法訪問實際對象。

 

 對象生命週期圖中添加三個新的可選狀態會造成一些困惑。邏輯順序上是從強可達到軟,弱和虛,最終到回收,但實際的情況取決於程序創建的參考對象。但如果創建WeakReference但不創建SoftReference,則對象直接從強可達到弱到達最終到收集。

強引用

強引用就是指在程序代碼之中普遍存在的,比如下面這段代碼中的obj和str都是強引用:

Object obj = new Object();
String str = "hello world";

只要強引用還存在,垃圾收集器永遠不會回收被引用的對象,即使在內存不足的情況下,JVM即使拋出OutOfMemoryError異常也不會回收這種對象。

實際使用上,可以通過把引用顯示賦值爲null來中斷對象與強引用之前的關聯,如果沒有任何引用執行對象,垃圾收集器將在合適的時間回收對象。

例如ArrayList類的remove方法中就是通過將引用賦值爲null來實現清理工作的:

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

引用對象

介紹軟引用、弱引用和虛引用之前,有必要介紹一下引用對象,

引用對象是程序代碼和其他對象之間的間接層,稱爲引用對象。每個引用對象都圍繞對象的引用構造,並且不能更改引用值。

 引用對象提供get()來獲得其引用值的一個強引用,垃圾收集器可能隨時回收引用值所指的對象。
一旦對象被回收,get()方法將返回null,要正確使用引用對象,下面使用SoftReference(軟引用對象)作爲參考示例:

    /**
     * 簡單使用demo
     */
    private static void simpleUseDemo(){
        List<String> myList = new ArrayList<>();
        SoftReference<List<String>> refObj = new SoftReference<>(myList);

        List<String> list = refObj.get();
        if (null != list) {
            list.add("hello");
        } else {
            // 整個列表已經被垃圾回收了,做其他處理
        }
    }

也就是說,使用時:

  • 1、必須經常檢查引用值是否爲null
    垃圾收集器可能隨時回收引用對象,如果輕率地使用引用值,遲早會得到一個NullPointerException。

  • 2、必須使用強引用來指向引用對象返回的值
    垃圾收集器可能在任何時間回收引用對象,即使在一個表達式中間。

    /**
     * 正確使用引用對象demo
     */
    private static void trueUseRefObjDemo(){
        List<String> myList = new ArrayList<>();
        SoftReference<List<String>> refObj = new SoftReference<>(myList);

        // 正確的使用,使用強引用指向對象保證獲得對象之後不會被回收
        List<String> list = refObj.get();
        if (null != list) {
            list.add("hello");
        } else {
            // 整個列表已經被垃圾回收了,做其他處理
        }
    }

    /**
     * 錯誤使用引用對象demo
     */
    private static void falseUseRefObjDemo(){
        List<String> myList = new ArrayList<>();
        SoftReference<List<String>> refObj = new SoftReference<>(myList);

        // XXX 錯誤的使用,在檢查對象非空到使用對象期間,對象可能已經被回收
        // 可能出現空指針異常
        if (null != refObj.get()) {
            refObj.get().add("hello");
        }
    }
  • 3、必須持有引用對象的強引用
    如果創建引用對象,沒有持有對象的強引用,那麼引用對象本身將被垃圾收集器回收。

  • 4、當引用值沒有被其他強引用指向時,軟引用、弱引用和虛引用纔會發揮作用,引用對象的存在就是爲了方便追蹤並高效垃圾回收。

軟引用、弱引用和虛引用 

引用對象的3個重要實現類位於java.lang.ref包下,分別是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。

軟引用

軟引用用來描述一些還有用但非必需的對象。對於軟引用關聯着的對象,在系統將要發生拋出OutOfMemoryError異常之前,將會把這些對象列入回收範圍之內進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出OutOfMemoryError異常。在JDK1.2之後,提供了SoftReference類來實現軟引用。

下面是一個使用示例:

import java.lang.ref.SoftReference;

public class SoftRefDemo {
    public static void main(String[] args) {
        SoftReference<String> sr = new SoftReference<>( new String("hello world "));
        // hello world
        System.out.println(sr.get());
    }
}

JDK文檔中提到:軟引用適用於對內存敏感的緩存:每個緩存對象都是通過訪問的 SoftReference,如果JVM決定需要內存空間,那麼它將清除回收部分或全部軟引用對應的對象。如果它不需要空間,則SoftReference指示對象保留在堆中,並且可以通過程序代碼訪問。在這種情況下,當它們被積極使用時,它們被強引用,否則會被軟引用。如果清除了軟引用,則需要刷新緩存。

實際使用上,要除非緩存的對象非常大,每個數量級爲幾千字節,才值得考慮使用軟引用對象。例如:實現一個文件服務器,它需要定期檢索相同的文件,或者需要緩存大型對象圖。如果對象很小,必須清除很多對象才能產生影響,那麼不建議使用,因爲清除軟引用對象會增加整個過程的開銷。

弱引用

弱引用也是用來描述非必需對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發送之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象

在JDK1.2之後,提供了WeakReference類來實現弱引用。

    /**
     * 簡單使用弱引用demo
     */
    private static void simpleUseWeakRefDemo(){
        WeakReference<String> sr = new WeakReference<>(new String("hello world " ));
        // before gc -> hello world 
        System.out.println("before gc -> " + sr.get());

        // 通知JVM的gc進行垃圾回收
        System.gc();
        // after gc -> null
        System.out.println("after gc -> " + sr.get());
    }

可以看到被弱引用關聯的對象,在gc之後被回收掉。
有意思的地方是,如果把上面代碼中的:

WeakReference<String> sr = new WeakReference<>(new String("hello world "));

改爲

WeakReference<String> sr = new WeakReference<>("hello world ");

程序將輸出

before gc -> hello world 
after gc -> hello world 

這是因爲使用Java的String直接賦值和使用new區別在於:

  • new 會在堆區創建一個可以被正常回收的對象。

  • String直接賦值,會在Java StringPool(字符串常量池)裏創建一個String對象,存於pergmen(永生代區)中,通常不會被gc回收。

WeakHashMap
爲了更方便使用弱引用,Java還提供了WeakHashMap,功能類似HashMap,內部實現是用弱引用對key進行包裝,當某個key對象沒有任何強引用指向,gc會自動回收key和value對象。

    /**
     *  weakHashMap使用demo
     */
    private static void weakHashMapDemo(){
        WeakHashMap<String,String> weakHashMap = new WeakHashMap<>();
        String key1 = new String("key1");
        String key2 = new String("key2");
        String key3 = new String("key3");
        weakHashMap.put(key1, "value1");
        weakHashMap.put(key2, "value2");
        weakHashMap.put(key3, "value3");

        // 使沒有任何強引用指向key1
        key1 = null;

        System.out.println("before gc weakHashMap = " + weakHashMap + " , size=" + weakHashMap.size());

        // 通知JVM的gc進行垃圾回收
        System.gc();
        System.out.println("after gc weakHashMap = " + weakHashMap + " , size="+ weakHashMap.size());
    }

程序輸出:

before: gc weakHashMap = {key1=value1, key2=value2, key3=value3} , size=3
after: gc weakHashMap = {key2=value2, key3=value3} , size=2

WeakHashMap比較適用於緩存的場景,例如Tomcat的緩存就用到。

引用隊列

介紹虛引用之前,先介紹引用隊列:
在使用引用對象時,通過判斷get()方法返回的值是否爲null來判斷對象是否已經被回收,當這樣做並不是非常高效,特別是當我們有很多引用對象,如果想找出哪些對象已經被回收,需要遍歷所有所有對象。

更好的方案是使用引用隊列,在構造引用對象時與隊列關聯,當gc(垃圾回收線程)準備回收一個對象時,如果發現它還僅有軟引用(或弱引用,或虛引用)指向它,就會在回收該對象之前,把這個軟引用(或弱引用,或虛引用)加入到與之關聯的引用隊列(ReferenceQueue)中。

如果一個軟引用(或弱引用,或虛引用)對象本身在引用隊列中,就說明該引用對象所指向的對象被回收了,所以要找出所有被回收的對象,只需要遍歷引用隊列。

當軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那麼這個引用對象本身就沒有價值了,如果程序中存在大量的這類對象(注意,我們創建的軟引用、弱引用、虛引用對象本身是個強引用,不會自動被gc回收),就會浪費內存。因此我們這就可以手動回收位於引用隊列中的引用對象本身。

    /**
     * 引用隊列demo
     */
    private static void refQueueDemo() {
        ReferenceQueue<String> refQueue = new ReferenceQueue<>();

        // 用於檢查引用隊列中的引用值被回收
        Thread checkRefQueueThread = new Thread(() -> {
            while (true) {
                Reference<? extends String> clearRef = refQueue.poll();
                if (null != clearRef) {
                    System.out
                            .println("引用對象被回收, ref = " + clearRef + ", value = " + clearRef.get());
                }
            }
        });
        checkRefQueueThread.start();

        WeakReference<String> weakRef1 = new WeakReference<>(new String("value1"), refQueue);
        WeakReference<String> weakRef2 = new WeakReference<>(new String("value2"), refQueue);
        WeakReference<String> weakRef3 = new WeakReference<>(new String("value3"), refQueue);

        System.out.println("ref1 value = " + weakRef1.get() + ", ref2 value = " + weakRef2.get()
                + ", ref3 value = " + weakRef3.get());

        System.out.println("開始通知JVM的gc進行垃圾回收");
        // 通知JVM的gc進行垃圾回收
        System.gc();
    }

程序輸出:

ref1 value = value1, ref2 value = value2, ref3 value = value3
開始通知JVM的gc進行垃圾回收
引用對象被回收, ref = java.lang.ref.WeakReference@48c6cd96, value=null
引用對象被回收, ref = java.lang.ref.WeakReference@46013afe, value=null
引用對象被回收, ref = java.lang.ref.WeakReference@423ea6e6, value=null

虛引用

虛引用也稱爲幽靈引用或者幻影引用,不同於軟引用和弱引用,虛引用不用於訪問引用對象所指示的對象,相反,通過不斷輪詢虛引用對象關聯的引用隊列,可以得到對象回收事件。一個對象是否有虛引用的存在,完全不會對其生產時間構成影響,也無法通過虛引用來取得一個對象實例。雖然這看起來毫無意義,但它實際上可以用來做對象回收時資源清理、釋放,它比finalize更靈活,我們可以基於虛引用做更安全可靠的對象關聯的資源回收。

  • finalize的問題

  • Java語言規範並不保證finalize方法會被及時地執行、而且根本不會保證它們會被執行
    如果可用內存沒有被耗盡,垃圾收集器不會運行,finalize方法也不會被執行。

  • 性能問題
    JVM通常在單獨的低優先級線程中完成finalize的執行。

  • 對象再生問題
    finalize方法中,可將待回收對象賦值給GC Roots可達的對象引用,從而達到對象再生的目的。

針對不靠譜finalize方法,完全可以使用虛引用來實現。在JDK1.2之後,提供了PhantomReference類來實現虛引用。

下面是簡單的使用例子,通過訪問引用隊列可以得到對象的回收事件:

    /**
     * 簡單使用虛引用demo
     * 虛引用在實現一個對象被回收之前必須做清理操作是很有用的,比finalize()方法更靈活
     */
    private static void simpleUsePhantomRefDemo() throws InterruptedException {
        Object obj = new Object();
        ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);

        // null
        System.out.println(phantomRef.get());
        // null
        System.out.println(refQueue.poll());

        obj = null;
        // 通知JVM的gc進行垃圾回收
        System.gc();

        // null, 調用phantomRef.get()不管在什麼情況下會一直返回null
        System.out.println(phantomRef.get());

        // 當GC發現了虛引用,GC會將phantomRef插入進我們之前創建時傳入的refQueue隊列
        // 注意,此時phantomRef對象,並沒有被GC回收,在我們顯式地調用refQueue.poll返回phantomRef之後
        // 當GC第二次發現虛引用,而此時JVM將phantomRef插入到refQueue會插入失敗,此時GC纔會對phantomRef對象進行回收
        Thread.sleep(200);
        Reference<?> pollObj = refQueue.poll();
        // java.lang.ref.PhantomReference@1540e19d
        System.out.println(pollObj);
        if (null != pollObj) {
            // 進行資源回收的操作
        }
    }

比較常見的,可以基於虛引用實現JDBC連接池,鎖的釋放等場景。
以連接池爲例,調用方正常情況下使用完連接,需要把連接釋放回池中,但是不可避免有可能程序有bug,造成連接沒有正常釋放回池中。基於虛引用對Connection對象進行包裝,並關聯引用隊列,就可以通過輪詢引用隊列檢查哪些連接對象已經被GC回收,釋放相關連接資源。具體實現已上傳github的caison-blog-demo倉庫。

總結

對比一下幾種引用對象的不同:

引用類型 GC回收時間 常見用途 生存時間
強引用 永不 對象的一般狀態 JVM停止運行時
軟引用 內存不足時 對象緩存 內存不足時終止
弱引用 GC時 對象緩存 GC後終止


虛引用,配合引用隊列使用,通過不斷輪詢引用隊列獲取對象回收事件。

雖然引用對象是一個非常有用的工具來管理你的內存消耗,但有時它們是不夠的,或者是過度設計的 。例如,使用一個Map來緩存從數據庫中讀取的數據。雖然可以使用弱引用來作爲緩存,但最終程序需要運行一定量的內存。如果不能給它足夠實際足夠的資源完成任何工作,那麼錯誤恢復機制有多強大也沒有用。

當遇到OutOfMemoryError錯誤,第一反應是要弄清楚它爲什麼會發生,也許真的是程序有bug,也許是可用內存設置的太低。

在開發過程中,應該制定程序具體的使用內存大小,而已要關注實際使用中用了多少內存。大多數應用程序在實際運行負載下,程序的內存佔用會達到穩定狀態,可以用此來作爲參考來設置合理的堆大小。如果程序的內存使用量隨着時間的推移而上升,很有可能是因爲當對象不再使用時仍然擁有對對象的強引用。引用對象在這裏可能會有所幫助,但更有可能是把它當做一個bug來進行修復。

文章所有涉及源碼已經上傳github,地址:https://github.com/caison/caison-blog-demo,可以點擊查看原文獲取。

本文轉自作者『陳彩華』

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章