本文經過探析Java中的引用模型,分析比較強引用、軟引用、弱引用、虛引用的概念及使用場景,知其然且知其因此然,但願給你們在實際開發實踐、學習開源項目提供參考。java
對於Java中的垃圾回收機制來講,對象是否被應該回收的取決於該對象是否被引用。所以,引用也是JVM進行內存管理的一個重要概念。Java中是JVM負責內存的分配和回收,這是它的優勢(使用方便,程序不用再像使用C語言那樣擔憂內存),但同時也是它的缺點(不夠靈活)。由此,Java提供了引用分級模型,能夠定義Java對象重要性和優先級,提升JVM內存回收的執行效率。git
關於引用的定義,在JDK1.2以前,若是reference類型的數據中存儲的數值表明的是另外一塊內存的起始地址,就稱爲這塊內存表明着一個引用;JDK1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。github
軟引用對象和弱應用對象主要用於:當內存空間還足夠,則能保存在內存之中;若是內存空間在垃圾收集以後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的使用場景。數據庫
而虛引用對象用於替代不靠譜的finalize方法,能夠獲取對象的回收事件,來作資源清理工做。緩存
## 2.1 無分級引用對象生命週期 前面提到,分層引用的模型是用於內存回收,沒有分級引用對象下,一個對象從建立到回收的生命週期能夠簡單地用下圖歸納:對象被建立,被使用,有資格被收集,最終被收集,陰影區域表示對象「強可達」時間:安全
2.2 有分級引用對象生命週期服務器
JDK1.2引入java.lang.ref程序包以後,對象的生命週期多了3個階段,軟可達,弱可達,虛可達,這些狀態僅適用於符合垃圾回收條件的對象,這些對象處於非強引用階段,並且須要基於java.lang.ref包中的相關的引用對象類來指示標明。架構
對象生命週期圖中添加三個新的可選狀態會形成一些困惑。邏輯順序上是從強可達到軟,弱和虛,最終到回收,但實際的狀況取決於程序建立的參考對象。但若是建立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 { // 整個列表已經被垃圾回收了,作其餘處理 } } 複製代碼
也就是說,使用時:
/** * 正確使用引用對象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個重要實現類位於java.lang.ref包下,分別是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。
5.1 軟引用
軟引用用來描述一些還有用但非必需的對象。對於軟引用關聯着的對象,在系統將要發生拋出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指示對象保留在堆中,而且能夠經過程序代碼訪問。在這種狀況下,當它們被積極使用時,它們被強引用,不然會被軟引用。若是清除了軟引用,則須要刷新緩存。
實際使用上,要除非緩存的對象很是大,每一個數量級爲幾千字節,才值得考慮使用軟引用對象。例如:實現一個文件服務器,它須要按期檢索相同的文件,或者須要緩存大型對象圖。若是對象很小,必須清除不少對象才能產生影響,那麼不建議使用,由於清除軟引用對象會增長整個過程的開銷。
5.2 弱引用
弱引用也是用來描述非必需對象,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發送以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
在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區別在於:
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的緩存就用到。
5.3 引用隊列
介紹虛引用以前,先介紹引用隊列: 在使用引用對象時,經過判斷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 複製代碼
5.4 虛引用
虛引用也稱爲幽靈引用或者幻影引用,不一樣於軟引用和弱引用,虛引用不用於訪問引用對象所指示的對象,相反,經過不斷輪詢虛引用對象關聯的引用隊列,能夠獲得對象回收事件。一個對象是否有虛引用的存在,徹底不會對其生產時間構成影響,也沒法經過虛引用來取得一個對象實例。雖然這看起來毫無心義,但它實際上能夠用來作對象回收時資源清理、釋放,它比finalize更靈活,咱們能夠基於虛引用作更安全可靠的對象關聯的資源回收。
針對不靠譜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來進行修復。
原文來源:https://juejin.im/post/5bbfee46e51d450e5e0cba2f
若是想學習(Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析) 的朋友能夠加個人Java架構羣:698581634,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。