標籤:
題記:說好的堅持一周兩篇文章在無數瑣事和自己的懶惰下沒有做好,在此表達一下對自己的不滿並對有嚴格執行力的人深表敬意!!!!
---------------------------------------------------------------------------------------------------------------------------------
引文:Java程式員對OutOfMemory並不陌生,一般來說,出現此異常主要是由於應用裡緩衝了大量的資料沒有被GC掉導致堆記憶體溢出,可是很多時候,為了減少重複計算或提升運行速度,必需要將一些資料緩衝起來,比如啟動的時候載入設定檔資訊、從資料庫裡初始化進來的資訊、運行過程中得到的一些中間結果等。程式員往JVM裡載入這些資料的時候往往會很糾結,一方面想緩衝的越多越好,盡量減少查庫和重複計算,但另一方面過多的緩衝對GC造成壓力,甚至要提心弔膽的考慮溢出的問題。
需求:如果能有一種方法可以儘可能的快取資料提高運行效率,又可以在GC前主動清空一部分到期資料從而防止記憶體溢出,該有多好。下面,我要講的基於Java軟引用實現堆記憶體監控,是筆者親身在生產系統的實踐,或許可以協助程式員在這方面做一些嘗試。
導讀:文章會先解釋什麼是軟引用,接著會說明GC對軟引用的處理特點,圍繞其特點利用JDK內建的相關類闡述代碼實現細節。
本文:
1. 什麼是軟引用:
我們知道,Java中有四種參考關聯性,分別是強引用、軟引用、弱引用、虛引用,如:
強引用:指JVM記憶體管理器從根引用集合(ROOt Set)出發遍尋堆中所有可到達的對象參考關聯性,也是常用的參考型別,如Object obj = new Object();只要強引用存在則GC時則必定不被回收。
軟引用:用來描述一些有用但並不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。如果一個對象只具有軟引用,則記憶體空間足夠,記憶體回收行程就不會回收它;如果記憶體空間不足了,就會回收這些對象的記憶體。
弱引用:用來描述非必需對象的,在java中,用java.lang.ref.WeakReference類來表示。當JVM進行記憶體回收時,無論記憶體是否充足,都會回收被弱引用關聯的對象。
虛引用:在任何時候都可能被記憶體回收行程回收的對象應用,用java.lang.ref.PhantomReference類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,此參考關聯性更多的是與虛引用隊列相關聯以方便做一些GC監控。
2. 軟引用特點
根據java協助文檔:"軟引用對象在響應記憶體需要時,由記憶體回收行程決定是否清除此對象。軟引用對象最常用於實現記憶體敏感的緩衝。假定記憶體回收行程確定在某一時間點某個對象是軟可到達對象。這時,它可以選擇自動清除針對該對象的所有軟引用,以及通過強引用鏈,從其可以到達該對象的針對任何其他軟可到達對象的所有軟引用。在同一時間或晚些時候,它會將那些已經向引用隊列註冊的新清除的軟引用排入佇列。軟可到達對象的所有軟引用都要保證在虛擬機器拋出 OutOfMemoryError 之前已經被清除。否則,清除軟引用的時間或者清除不同對象的一組此類引用的順序將不受任何約束。然而,虛擬機器實現不鼓勵清除最近訪問或使用過的軟引用。此類的直接執行個體可用於實現簡單緩衝;該類或其派生的子類還可用於更大型的資料結構,以實現更複雜的緩衝。只要軟引用的指示對象是強可到達對象,即正在實際使用的對象,就不會清除軟引用。例如,通過保持最近使用的項的強指示對象,並由記憶體回收行程決定是否放棄剩餘的項,複雜的緩衝可以防止放棄最近使用的項。"
根據上述內容我們知道,軟引用對象會在OutOfMemoryError 之前由JVM保證將其回收,並把它加入到其註冊的清除隊列中。因此,通過監控該隊列是否有即將被清除的軟引用對象,我們就可以間接得知java應用是否已經到溢出崩潰邊緣了,並在其溢出前迅速執行部分快取資料的清空工作從而讓虛擬機器可以清理出一些記憶體出來避免堆記憶體的溢出,更進一步想,我們可以將該軟引用對象設定成佔一定記憶體大小的對象,如10M,這樣當虛擬機器記憶體不足時會第一時間將此對象回收進而騰出10M空餘記憶體,進而緩解記憶體不足,同時為應用爭取了寶貴清空部分快取資料的時間,有效避免直接拋出記憶體溢出的異常。
3. 實現細節
根據上面的分析和實際的開發實踐,利用軟引用對象監控虛擬機器記憶體使用量情況的代碼實現如下:
- 初始化軟引用對象與引用隊列,並設定軟引用對象佔用10M的記憶體
1 //記憶體監控2 public static ReferenceQueue<byte[]> memoryDetectorQueue ; 3 public static SoftReference<byte[]> memoryDetector;4 5 // initial 6 public static void initial(){ 7 memoryDetectorQueue = new ReferenceQueue<byte[]>();8 memoryDetector = new SoftReference<byte[]>(new byte[(int)(10*1024*1024)],memoryDetectorQueue);9 }
- 設定一個單獨的線程,並在軟引用對象初始化後啟動該線程,開始監視memoryDetectorQueue是否非空,非空則說明軟引用對象由於記憶體空間不夠被清理,記憶體告急:
1 public class MemoryMonitorService implements Runnable { 2 3 public void run() { 4 while (true) { 5 try { 6 if (memoryDetectorQueue.remove() != null) { 7 doPartClean(); //執行部分緩衝的清空以釋放記憶體,可以根據一些LRU演算法或按比例來執行清理 8 } 9 } catch (Exception e) {10 logger.error("", e);11 }finally{12 memoryDetector = new SoftReference<byte[]>(new byte[(int) (10 * 1024 * 1024)],13 memoryDetectorQueue); // 執行完部分緩衝清理後重新建立軟引用對象14 }15 }16 }17 }說明:memoryDetectorQueue.remove()方法會一直等待,阻塞到某個對象變得可用為止,它返回的值不為空白時說明memoryDetector 軟引用對象被GC掉了。
- 調用new MemoryMonitorService().start()啟動監控線程。一般來說,上面代碼裡的doPartClean()工作是由專門的清理類來輔助的。
基於Java軟引用機制最大使用JVM堆記憶體並杜絕OutOfMemory