Java 記憶體洩露的理解與解決過程
本文詳細地介紹了Java記憶體管理的原理,以及記憶體洩露產生的原因,同時提供了一些列解決Java記憶體洩露的方案,希望對各位Java開發者有所幫助。
Java記憶體管理機制
在C++ 語言中,如果需要動態分配一塊記憶體,程式設計師需要負責這塊記憶體的整個生命週期。從申請分配、到使用、再到最後的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程式設計師很容易由於疏忽而忘記釋放記憶體,從而導致記憶體的洩露。 Java 語言對記憶體管理做了自己的優化,這就是垃圾回收機制。 Java 的幾乎所有記憶體物件都是在堆記憶體上分配(基本資料型別除外),然後由 GC ( garbage collection)負責自動回收不再使用的記憶體。
上面是Java 記憶體管理機制的基本情況。但是如果僅僅理解到這裡,我們在實際的專案開發中仍然會遇到記憶體洩漏的問題。也許有人表示懷疑,既然 Java 的垃圾回收機制能夠自動的回收記憶體,怎麼還會出現記憶體洩漏的情況呢?這個問題,我們需要知道 GC 在什麼時候回收記憶體物件,什麼樣的記憶體物件會被 GC 認為是“不再使用”的。
Java中對記憶體物件的訪問,使用的是引用的方式。在 Java 程式碼中我們維護一個記憶體物件的引用變數,通過這個引用變數的值,我們可以訪問到對應的記憶體地址中的記憶體物件空間。在 Java 程式中,這個引用變數本身既可以存放堆記憶體中,又可以放在程式碼棧的記憶體中(與基本資料型別相同)。 GC 執行緒會從程式碼棧中的引用變數開始跟蹤,從而判定哪些記憶體是正在使用的。如果 GC 執行緒通過這種方式,無法跟蹤到某一塊堆記憶體,那麼 GC 就認為這塊記憶體將不再使用了(因為程式碼中已經無法訪問這塊記憶體了)。
通過這種有向圖的記憶體管理方式,當一個記憶體物件失去了所有的引用之後,GC 就可以將其回收。反過來說,如果這個物件還存在引用,那麼它將不會被 GC 回收,哪怕是 Java 虛擬機器丟擲 OutOfMemoryError 。
Java記憶體洩露
可能光說概念太抽象了,大家可以看一下這樣的例子:
Vector v = new Vector( 10 ); for ( int i = 1 ;i < 100 ; i ++ ){ Object o = new Object(); v.add(o); o = null ; }
在這個例子中,程式碼棧中存在Vector 物件的引用 v 和 Object 物件的引用 o 。在 For 迴圈中,我們不斷的生成新的物件,然後將其新增到 Vector 物件中,之後將 o 引用置空。問題是當 o 引用被置空後,如果發生 GC ,我們建立的 Object 物件是否能夠被 GC 回收呢?答案是否定的。因為, GC 在跟蹤程式碼棧中的引用時,會發現 v 引用,而繼續往下跟蹤,就會發現 v 引用指向的記憶體空間中又存在指向 Object 物件的引用。也就是說盡管 o 引用已經被置空,但是 Object 物件仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此迴圈之後, Object 物件對程式已經沒有任何作用,那麼我們就認為此 Java 程式發生了記憶體洩漏。
儘管對於C/C++ 中的記憶體洩露情況來說, Java 記憶體洩露導致的破壞性小,除了少數情況會出現程式崩潰的情況外,大多數情況下程式仍然能正常執行。但是,在移動裝置對於記憶體和 CPU都有較嚴格的限制的情況下, Java 的記憶體溢位會導致程式效率低下、佔用大量不需要的記憶體等問題。這將導致整個機器效能變差,嚴重的也會引起丟擲 OutOfMemoryError ,導致程式崩潰。
一般情況下記憶體洩漏的避免
在不涉及複雜資料結構的一般情況下,Java 的記憶體洩露表現為一個記憶體物件的生命週期超出了程式需要它的時間長度。我們有時也將其稱為“物件遊離”。
例如:
public class FileSearch{ private byte [] content; private File mFile; public FileSearch(File file){ mFile = file; } public boolean hasString(String str){ int size = getFileSize(mFile); content = new byte [size]; loadFile(mFile, content); String s = new String(content); return s.contains(str); } }
在這段程式碼中,FileSearch 類中有一個函式 hasString ,用來判斷文件中是否含有指定的字串。流程是先將mFile 載入到記憶體中,然後進行判斷。但是,這裡的問題是,將 content 宣告為了例項變數,而不是本地變數。於是,在此函式返回之後,記憶體中仍然存在整個檔案的資料。而很明顯,這些資料我們後續是不再需要的,這就造成了記憶體的無故浪費。
要避免這種情況下的記憶體洩露,要求我們以C/C++ 的記憶體管理思維來管理自己分配的記憶體。第一,是在宣告物件引用之前,明確記憶體物件的有效作用域。在一個函式內有效的記憶體物件,應該宣告為 local 變數,與類例項生命週期相同的要宣告為例項變數……以此類推。第二,在記憶體物件不再需要時,記得手動將其引用置空。
複雜資料結構中的記憶體洩露問題
在實際的專案中,我們經常用到一些較為複雜的資料結構用於快取程式執行過程中需要的資料資訊。有時,由於資料結構過於複雜,或者我們存在一些特殊的需求(例如,在記憶體允許的情況下,儘可能多的快取資訊來提高程式的執行速度等情況),我們很難對資料結構中資料的生命週期作出明確的界定。這個時候,我們可以使用Java 中一種特殊的機制來達到防止記憶體洩露的目的。
之前我們介紹過,Java 的 GC 機制是建立在跟蹤記憶體的引用機制上的。而在此之前,我們所使用的引用都只是定義一個“ Object o; ”這樣形式的。事實上,這只是 Java 引用機制中的一種預設情況,除此之外,還有其他的一些引用方式。通過使用這些特殊的引用機制,配合 GC 機制,就可以達到一些我們需要的效果。
Java中的幾種引用方式
Java中有幾種不同的引用方式,它們分別是:強引用、軟引用、弱引用和虛引用。下面,我們首先詳細地瞭解下這幾種引用方式的意義。
強引用
在此之前我們介紹的內容中所使用的引用 都是強引用,這是使用最普遍的引用。如果一個物件具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空 間不足,Java 虛擬機器寧願丟擲 OutOfMemoryError 錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。
軟引用(SoftReference )
SoftReference 類的一個典型用途就是用於記憶體敏感的快取記憶體。 SoftReference 的原理是:在保持對物件的引用時保證在 JVM 報告記憶體不足情況之前將清除所有的軟引用。關鍵之處在於,垃圾收集器在執行時可能會(也可能不會)釋放軟可及物件。物件是否被釋放取決於垃圾收集器的演算法 以及垃圾收集器執行時可用的記憶體數量。
弱引用(WeakReference )
WeakReference 類的一個典型用途就是規範化對映( canonicalized mapping )。另外,對於那些生存期相對較長而且重新建立的開銷也不高的物件來說,弱引用也比較有用。關鍵之處在於,垃圾收集器執行時如果碰到了弱可及物件,將釋放 WeakReference 引用的物件。然而,請注意,垃圾收集器可能要執行多次才能找到並釋放弱可及物件。
虛引用(PhantomReference )
PhantomReference 類只能用於跟蹤對被引用物件即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。 PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。當垃圾收集器確定了某個物件是虛可及物件時, PhantomReference 物件就被放在它的 ReferenceQueue 上。將 PhantomReference 物件放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 物件引用的物件已經結束,可供收集了。這使您能夠剛好在物件佔用的記憶體被回收之前採取行動。 Reference與 ReferenceQueue 的配合使用。
GC、 Reference 與 ReferenceQueue 的互動
3、軟引用和弱引用在新增到 ReferenceQueue 的時候,其指向真實記憶體的引用已經被置為空了,相關的記憶體也已經被釋放掉了。而虛引用在新增到 ReferenceQueue 的時候,記憶體還沒有釋放,仍然可以對其進行訪問。
通過以上的介紹,相信您對Java 的引用機制以及幾種引用方式的異同已經有了一定了解。光是概念,可能過於抽象,下面我們通過一個例子來演示如何在程式碼中使用 Reference 機制。
String str = new String( " hello " ); // ① ReferenceQueue < String > rq = new ReferenceQueue < String > (); // ② WeakReference < String > wf = new WeakReference < String > (str, rq); // ③ str = null ; // ④取消"hello"物件的強引用 String str1 = wf.get(); // ⑤假如"hello"物件沒有被回收,str1引用"hello"物件 // 假如"hello"物件沒有被回收,rq.poll()返回null Reference <? extends String > ref = rq.poll(); // ⑥
在以上程式碼中,注意⑤⑥兩處地方。假如“hello ”物件沒有被回收 wf.get() 將返回“ hello ”字串物件, rq.poll() 返回 null ;而加入“ hello ”物件已經被回收了,那麼 wf.get() 返回 null , rq.poll() 返回 Reference 物件,但是此 Reference 物件中已經沒有 str 物件的引用了 ( PhantomReference 則與WeakReference 、 SoftReference 不同 )。
引用機制與複雜資料結構的聯合應用
瞭解了GC 機制、引用機制,並配合上 ReferenceQueue ,我們就可以實現一些防止記憶體溢位的複雜資料型別。
例如,SoftReference 具有構建 Cache 系統的特質,因此我們可以結合雜湊表實現一個簡單的快取系統。這樣既能保證能夠儘可能多的快取資訊,又可以保證 Java 虛擬機器不會因為記憶體洩露而丟擲 OutOfMemoryError 。這種快取機制特別適合於記憶體物件生命週期長,且生成記憶體物件的耗時比較長的情況,例如快取列表封面圖片等。對於一些生命週期較長,但是生成記憶體物件開銷不大的情況,使用WeakReference 能夠達到更好的記憶體管理的效果。
package com. *** .widget; // : SoftHashMap.java import java.util. * ; import java.lang.ref. * ; import android.util.Log; public class SoftHashMap extends AbstractMap { /** The internal HashMap that will hold the SoftReference. */ private final Map hash = new HashMap(); /** The number of "hard" references to hold internally. */ private final int HARD_SIZE; /** The FIFO list of hard references, order of last access. */ private final LinkedList hardCache = new LinkedList(); /** Reference queue for cleared SoftReference objects. */ private ReferenceQueue queue = new ReferenceQueue(); // Strong Reference number public SoftHashMap() { this ( 100 ); } public SoftHashMap( int hardSize) { HARD_SIZE = hardSize; } public Object get(Object key) { Object result = null ; // We get the SoftReference represented by that key SoftReference soft_ref = (SoftReference)hash.get(key); if (soft_ref != null ) { // From the SoftReference we get the value, which can be // null if it was not in the map, or it was removed in // the processQueue() method defined below result = soft_ref.get(); if (result == null ) { // If the value has been garbage collected, remove the // entry from the HashMap. hash.remove(key); } else { // We now add this object to the beginning of the hard // reference queue. One reference can occur more than // once, because lookups of the FIFO queue are slow, so // we don't want to search through it each time to remove // duplicates. // keep recent use object in memory hardCache.addFirst(result); if (hardCache.size() > HARD_SIZE) { // Remove the last entry if list longer than HARD_SIZE hardCache.removeLast(); } } } return result; } /** We define our own subclass of SoftReference which contains not only the value but also the key to make it easier to find the entry in the HashMap after it's been garbage collected. */ private static class SoftValue extends SoftReference { private final Object key; // always make data member final /** Did you know that an outer class can access private data members and methods of an inner class? I didn't know that! I thought it was only the inner class who could access the outer class's private information. An outer class can also access private members of an inner class inside its inner class. */ private SoftValue(Object k, Object key, ReferenceQueue q) { super (k, q); this .key = key; } } /** Here we go through the ReferenceQueue and remove garbage collected SoftValue objects from the HashMap by looking them up using the SoftValue.key data member. */ public void processQueue() { SoftValue sv; while ((sv = (SoftValue)queue.poll()) != null ) { if (sv.get() == null ) { Log.e( " processQueue " , " null " ); } else { Log.e( " processQueue " , " Not null " ); } hash.remove(sv.key); // we can access private data! Log.e( " SoftHashMap " , " release " + sv.key); } } /** Here we put the key, value pair into the HashMap using a SoftValue object. */ public Object put(Object key, Object value) { processQueue(); // throw out garbage collected values first Log.e( " SoftHashMap " , " put into " + key); return hash.put(key, new SoftValue(value, key, queue)); } public Object remove(Object key) { processQueue(); // throw out garbage collected values first return hash.remove(key); } public void clear() { hardCache.clear(); processQueue(); // throw out garbage collected values hash.clear(); } public int size() { processQueue(); // throw out garbage collected values first return hash.size(); } public Set entrySet() { // no, no, you may NOT do that!!! GRRR throw new UnsupportedOperationException(); } }
相關文章
- JAVA記憶體洩露的原因及解決Java記憶體洩露
- 記一次"記憶體洩露"排查過程記憶體洩露
- 解決git記憶體洩露問題Git記憶體洩露
- Android 記憶體洩露詳解Android記憶體洩露
- ArkTS 的記憶體快照與記憶體洩露除錯記憶體洩露除錯
- 記一則伺服器記憶體洩漏解決過程伺服器記憶體
- Java記憶體洩漏解決之道Java記憶體
- 記一次透過Memory Analyzer分析記憶體洩漏的解決過程記憶體
- php常駐程式記憶體洩露的簡單解決PHP記憶體洩露
- SHBrowseForFolder 記憶體洩露記憶體洩露
- react 記憶體洩露常見問題解決方案React記憶體洩露
- java中如何檢視記憶體洩露Java記憶體洩露
- 記憶體溢位和記憶體洩露記憶體溢位記憶體洩露
- Lowmemorykiller記憶體洩露分析記憶體洩露
- win10驅動記憶體洩露如何解決_win10記憶體洩露處理方法Win10記憶體洩露
- 記一次 Java 應用記憶體洩漏的定位過程Java記憶體
- JS高程中的垃圾回收機制與常見記憶體洩露的解決方法JS記憶體洩露
- 一次排查Java專案記憶體洩漏的過程Java記憶體
- 翻譯 | 理解Java中的記憶體洩漏Java記憶體
- 一次Kafka記憶體洩露排查經過Kafka記憶體洩露
- 使用 mtrace 分析 “記憶體洩露”記憶體洩露
- 實戰Go記憶體洩露Go記憶體洩露
- Android中使用Handler造成記憶體洩露的分析和解決Android記憶體洩露
- android Handler導致的記憶體洩露Android記憶體洩露
- ThreadLocal原始碼解讀和記憶體洩露分析thread原始碼記憶體洩露
- 一次 Java 記憶體洩漏排查過程,漲姿勢Java記憶體
- 記一次尷尬的Java應用記憶體洩露排查Java記憶體洩露
- Linux記憶體洩露案例分析和記憶體管理分享Linux記憶體洩露
- nodejs爬蟲記憶體洩露排查NodeJS爬蟲記憶體洩露
- Pprof定位Go程式記憶體洩露Go記憶體洩露
- 記錄一次記憶體洩漏排查過程記憶體
- 一行程式碼教你解決FlutterPlatformViews記憶體洩露(memory leak)行程FlutterPlatformView記憶體洩露
- 簡單的記憶體“洩露”和“溢位”記憶體
- Python實現記憶體洩露排查的示例Python記憶體洩露
- 記一次使用windbg排查記憶體洩漏的過程記憶體
- 清晰勝過聰明: 改進 flatbuffers-go[更新記憶體洩露與 GC]Go記憶體洩露GC
- Java記憶體洩漏Java記憶體
- netty 堆外記憶體洩露排查盛宴Netty記憶體洩露
- 乾貨分享:淺談記憶體洩露記憶體洩露