JVM調優(4)之垃圾回收面臨的問題
如何區分垃圾
引用計數演算法:
來記錄一個物件被引用的次數,當引用計數器為0時,代表這個物件不再被使用。
優點:實現簡單,判斷效率也很高。
缺點:它很難解決物件之間相互迴圈引用的問題。
可達性分析演算法:
在主流的商用程式語言的主流實現都是通過可達性分析來判斷物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用相連時,證明此物件是不可用的。
java語言中,可作為GC Roots的物件包括下面幾種:
①虛擬機器棧(棧幀中的本地變量表)中引用的物件。
②方法區中靜態屬性引用的物件。
③方法區中常量引用的物件。
④本地方法棧中JNI(即一般說的Native方法)引用的物件。
垃圾回收從哪兒開始的呢?
從哪兒開始查詢哪些物件是正在被當前系統使用的。其中棧是真正進行程式執行地方,所以要獲取哪些物件正在被使用,則需要從Java棧開始。同時,一個棧是與一個執行緒對應的,因此,如果有多個執行緒的話,則必須對這些執行緒對應的所有的棧進行檢查。
同時,除了棧外,還有系統執行時的暫存器等,也是儲存程式執行資料的。這樣,以棧或暫存器中的引用為起點,我們可以找到堆中的物件,又從這些物件找到對堆中其他物件的引用,這種引用逐步擴充套件,最終以null引用或者基本型別結束,這樣就形成了一顆以Java棧中引用所對應的物件為根節點的一顆物件樹,如果棧中有多個引用,則最終會形成多顆物件樹。在這些物件樹上的物件,都是當前系統執行所需要的物件,不能被垃圾回收。而其他剩餘物件,則可以視為無法被引用到的物件,可以被當做垃圾進行回收。
因此,垃圾回收的起點是一些根物件(java棧, 靜態變數, 暫存器…)。而最簡單的Java棧就是Java程式執行的main函式。這種回收方式,也是上面提到的“標記-清除”的回收方式
記憶體洩漏例項及解決方案
什麼是Java中的記憶體洩露
java已經有垃圾回收,為什麼還會出現記憶體洩漏已經在上篇博文裡第六點提到了,請檢視
JVM調優之垃圾回收
Java記憶體洩漏的型別、例項及解決
1.物件遊離
public Item pop(){//刪除棧頂元素
Item item = a[--N];
a[N] = null;//**避免物件遊離**
...
return item;
}
Java的垃圾收集策略是回收所有無法被訪問的物件的記憶體。在我們對pop()的實現中,被彈出的元素的引用仍然存在於陣列中。這個元素實際上就是個孤兒了,沒有誰會再訪問它,但Java編譯器沒法知道這一點,除非該引用被覆蓋。這種情況(儲存一個不需要的物件的引用)成為遊離。在這裡,避免物件遊離很簡單,只需將被彈出的陣列元素的值設為null即可,這將覆蓋無用的引用,並使系統在使用完被彈出的元素後回收它的記憶體
清單 1. 展示 “物件遊離” 的類
public class LeakyChecksum {
private byte[] byteArray;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len];
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
這個類存在很多的問題,但是我們著重來看記憶體洩漏。快取緩衝區的決定很可能是根據這樣的假設得出的,即該類將在一個程式中被呼叫許多次,因此它應該更加有效,以重用緩衝區而不是重新分配它。但是結果是,緩衝區永遠不會被釋放,因為它對程式來說總是可及的(除非 LeakyChecksum 物件被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大檔案一樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,並且要求更頻繁的收集;為計算未來的校驗和而保持一個大型緩衝區並不是可用記憶體的最有效利用。
LeakyChecksum 中問題的原因是,緩衝區對於 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命週期已經被人為延長了,因為將它提升到了例項欄位。因此,該類必須自己管理緩衝區的生命週期,而不是讓 JVM 來管理。
2. 軟引用
在Java中,雖然不需要程式設計師手動去管理物件的生命週期,但是如果希望某些物件具備一定的生命週期的話(比如記憶體不足時JVM就會自動回收某些物件從而避免OutOfMemory的錯誤)就需要用到軟引用和弱引用了。
從Java SE2開始,就提供了四種類型的引用:強引用、軟引用、弱引用和虛引用。
這四種引用的概念請參考:JVM調優之基本概念
Java中提供這四種引用型別主要有兩個目的:
第一:可以讓程式設計師通過程式碼的方式決定某些物件的生命週期;
第二:有利於JVM進行垃圾回收。
對於強引用,我們平時在編寫程式碼時經常會用到。而對於其他三種類型的引用,使用得最多的就是軟引用和弱引用,這2種既有相似之處又有區別。它們都是用來描述非必需物件的,但是被軟引用關聯的物件只有在記憶體不足時才會被回收,而被弱引用關聯的物件在JVM進行垃圾回收時總會被回收。
在SoftReference類中,有三個方法,兩個構造方法和一個get方法(WekReference類似):
兩個構造方法:
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
get方法用來獲取與軟引用關聯的物件的引用,如果該物件被回收了,則返回null。
在使用軟引用和弱引用的時候,我們可以顯示地通過System.gc()來通知JVM進行垃圾回收,但是要注意的是,雖然發出了通知,JVM不一定會立刻執行,也就是說這句是無法確保此時JVM一定會進行垃圾回收的。
如何利用軟引用和弱引用解決OOM問題?
前面講了關於軟引用和弱引用相關的基礎知識,那麼到底如何利用它們來優化程式效能,從而避免OOM的問題呢?
下面舉個例子,假如有一個應用需要讀取大量的本地圖片,如果每次讀取圖片都從硬碟讀取,則會嚴重影響效能,但是如果全部載入到記憶體當中,又有可能造成記憶體溢位,此時使用軟引用可以解決這個問題。
設計思路是:用一個HashMap來儲存圖片的路徑 和 相應圖片物件關聯的軟引用之間的對映關係,在記憶體不足時,JVM會自動回收這些快取圖片物件所佔用的空間,從而有效地避免了OOM的問題。在Android開發中對於大量圖片下載會經常用到。
下面這段程式碼是摘自部落格:
http://blog.csdn.net/arui319/article/details/8489451
首先定義一個HashMap,儲存軟引用物件。
private Map<String, SoftReference<Bitmap>> imageCache =
new HashMap<String, SoftReference<Bitmap>>();
再來定義一個方法,儲存Bitmap的軟引用到HashMap。
public void addBitmapToCache(String path) {
// 強引用的Bitmap物件
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 軟引用的Bitmap物件
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 新增該物件到Map中使其快取
imageCache.put(path, softBitmap);
}
獲取的時候,可以通過SoftReference的get()方法得到Bitmap物件。
public Bitmap getBitmapByPath(String path) {
// 從快取中取軟引用的Bitmap物件
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 判斷是否存在軟引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap物件,如果由於記憶體不足Bitmap被回收,將取得空
Bitmap bitmap = softBitmap.get();
return bitmap;
}
使用軟引用以後,在OutOfMemory異常發生之前,這些快取的圖片資源的記憶體空間可以被釋放掉的,從而避免記憶體達到上限,避免Crash發生。
需要注意的是,在垃圾回收器對這個Java物件回收前,SoftReference類所提供的get方法會返回Java物件的強引用,一旦垃圾執行緒回收該Java物件之後,get方法將返回null。所以在獲取軟引用物件的程式碼中,一定要判斷是否為null,以免出現NullPointerException異常導致應用崩潰。
3.基於陣列的集合
當陣列用於實現諸如堆疊或環形緩衝區之類的資料結構時,會出現另一種形式的物件遊離。清單 3 中的 LeakyStack 類展示了用陣列實現的堆疊的實現。在 pop() 方法中,在頂部指標遞減之後,elements 仍然會保留對將彈出堆疊的物件的引用。這意味著,該物件的引用對程式來說仍然可及(即使程式實際上不會再使用該引用),這會阻止該物件被垃圾收集,直到該位置被未來的 push() 重用。
清單 3. 基於陣列的集合中的物件遊離
public class LeakyStack {
private Object[] elements = new Object[MAX_ELEMENTS];
private int size = 0;
public void push(Object o) { elements[size++] = o; }
public Object pop() {
if (size == 0)
throw new EmptyStackException();
else {
Object result = elements[--size];
// elements[size+1] = null;
return result;
}
}
}
修復這種情況下的物件遊離的方法是,當物件從堆疊彈出之後,就消除它的引用,如清單 3 中註釋掉的行所示。但是這種情況 —— 由類管理其自己的記憶體 —— 是一種非常少見的情況,即顯式地消除不再需要的物件是一個好主意。大部分時候,認為不應該使用的強行消除引用根本不會帶來效能或記憶體使用方面的收益,通常是導致更差的效能或者 NullPointerException。該演算法的一個連結實現不會存在這個問題。在連結實現中,連結節點(以及所儲存的物件的引用)的生命期將被自動與物件儲存在集合中的期間繫結在一起。弱引用可用於解決這個問題 —— 維護弱引用而不是強引用的一個數組 —— 但是在實際中,LeakyStack 管理它自己的記憶體,因此負責確保對不再需要的物件的引用被清除。使用陣列來實現堆疊或緩衝區是一種優化,可以減少分配,但是會給實現者帶來更大的負擔,需要仔細地管理儲存在陣列中的引用的生命期。
如何處理碎片
由於不同Java物件存活時間是不一定的,因此,在程式執行一段時間以後,如果不進行記憶體整理,就會出現零散的記憶體碎片。碎片最直接的問題就是會導致無法分配大塊的記憶體空間,以及程式執行效率降低。所以,在上面提到的基本垃圾回收演算法中,“複製”方式和“標記-整理”方式,都可以解決碎片的問題。
如何解決同時存在的物件建立和物件回收問題
垃圾回收執行緒是回收記憶體的,而程式執行執行緒則是消耗(或分配)記憶體的,一個回收記憶體,一個分配記憶體,從這點看,兩者是矛盾的。因此,在現有的垃圾回收方式中,要進行垃圾回收前,一般都需要暫停整個應用(即:暫停記憶體的分配),然後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,而且最有效的解決二者矛盾的方式。
但是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,比如最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就很有可能超過這個限制,在這種情況下,垃圾回收將會成為系統執行的一個瓶頸。為解決這種矛盾,有了併發垃圾回收演算法,使用這種演算法,垃圾回收執行緒與程式執行執行緒同時執行。在這種方式下,解決了暫停的問題,但是因為需要在新生成物件的同時又要回收物件,演算法複雜性會大大增加,系統的處理能力也會相應降低,同時,“碎片”問題將會比較難解決。
參考文件
Java 記憶體管理原理、記憶體洩漏例項及解決方案研究
Java 如何有效地避免OOM:善於利用軟引用和弱引用
Android開發優化之——使用軟引用和弱引用
VM調優總結(四)-垃圾回收面臨的問題