1. 程式人生 > >JVM調優(8)Java的記憶體洩漏

JVM調優(8)Java的記憶體洩漏

記憶體溢位和記憶體洩漏

記憶體溢位
out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;

記憶體洩露
memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。
memory leak會最終會導致out of memory!

以發生的方式來分類,記憶體洩漏可以分為4類:

  • 常發性記憶體洩漏: 發生記憶體洩漏的程式碼會被多次執行到,每次被執行的時候都會導致一塊記憶體洩漏。
  • 偶發性記憶體洩漏: 發生記憶體洩漏的程式碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體洩漏至關重要。
  • 一次性記憶體洩漏: 發生記憶體洩漏的程式碼只會被執行一次,或者由於演算法上的缺陷,導致總會有一塊僅且一塊記憶體發生洩漏。比如,在類的建構函式中分配記憶體,在解構函式中卻沒有釋放該記憶體,所以記憶體洩漏只會發生一次。
  • 隱式記憶體洩漏: 程式在執行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要執行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體洩漏為隱式記憶體洩漏。

Java記憶體回收方式

Java判斷物件是否可以回收使用的而是可達性分析演算法。


但對於不同生命週期的內回收方式不同,這部分可以參考:JVM調優
這個演算法的基本思路就是通過一系列名為"GC Roots"的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,下圖物件object3, object4, object5雖然有互相判斷,但它們到GC Roots是不可達的,所以它們將會判定為是可回收物件。
在這裡插入圖片描述

問題的提出

通過GC程式設計師大部分情況下是不需要關心記憶體佔用和釋放問題。但是,如果程式寫的不合理,也可以說不正確的話,或者GC,JVM設定不合理。系統就有可能也存在記憶體洩露。

隨著越來越多的伺服器程式採用Java技術,叢集,分散式,多執行緒等,伺服器程式往往長期執行,有時產品釋出活動多併發等。記憶體洩露問題也就變得十分關鍵,即使每次執行少量洩漏,長期執行之後,系統也是面臨崩潰的危險。

記憶體洩漏引起的原因

為什麼會有記憶體洩露呢?因為無用物件持續佔有記憶體或無用物件的記憶體得不到及時釋放,從而造成的記憶體空間的浪費而引起的。記憶體洩露有時不嚴重且不易察覺,這樣開發者就不知道存在記憶體洩露,但有時也會很嚴重,會提示你Out of memory。

那麼,Java記憶體洩露根本原因是什麼呢?長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩露,儘管短生命週期物件已經不再需要,但是因為長生命週期物件持有它的引用而導致不能被回收,這就是java中記憶體洩露的發生場景。具體主要有如下幾大類

1、各種連線。

比如IO連線,網路連線和資料庫連線,除非其顯式的呼叫了其close()方法將其連線關閉,否則是不會自動被GC回收的。對於Resultset和Statement物件可以不進行顯式回收,但Connection一定要顯式回收,因為Connection在任何時候都無法自動回收,而Connection一旦回收,Resultset和Statement物件就會立即為NULL。但是如果使用連線池,情況就不一樣了,除了要顯式地關閉連線,還必須顯式地關閉Resultset Statement物件(關閉其中一個,另外一個也會關閉,否則就會造成大量的Statement物件無法釋放,從而引起記憶體洩漏。這種情況下一般都會在try裡面去的連線,在finally裡面釋放連線。

2、單例模式

不正確使用單例模式是引起記憶體洩露的一個常見問題,單例物件在被初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部物件的引用,那麼這個外部物件將不能被jvm正常回收,導致記憶體洩露,考慮下面的例子:

3、監聽器

在java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會呼叫一個控制元件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放物件的時候卻沒有記住去刪除這些監聽器,從而增加了記憶體洩漏的機會。

4、靜態集合類引起記憶體洩露

像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,他們所引用的所有的物件Object也不能被釋放,因為他們也將一直被Vector等引用著。

5、無界限阻塞佇列

如果使用像LinkedBlockingQueue這樣的無界的阻塞佇列,如果消費者業務處理很慢,而生產者往佇列中新增物件速度快,無界的阻塞佇列就無限長,就可能導致記憶體不足。

6、JDK6/7的subString()

在JDK6中subString是是通過改變offset和count來建立一個新的String物件,value相當於一個倉庫,還是用的父String的。

依JDK7對subString實現的原始碼為例:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
      throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
      throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
      throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
  }
public String(char value[], int offset, int count) {
    if (offset < 0) {
      throw new StringIndexOutOfBoundsException(offset);
    }
    if (count < 0) {
      throw new StringIndexOutOfBoundsException(count);
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
      throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
  }
public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        char[] copy = new char[newLength];   //是建立了一個新的char陣列
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }

例子

String str1 = "12rgabu94";
String str2 = str.substring(0, 3);
str1 = null;

雖然str1為null,但是由於str1和str2之前指向的物件用的是同一個value陣列,導致第一個String物件無法被GC,這樣就可能形成了記憶體洩漏。具體的講解請參考:The substring() Method in JDK 6 and JDK 7
1.6和1.7的記憶體變化如圖:
在這裡插入圖片描述

如何避免java記憶體洩漏?

1、儘早釋放無用物件的引用。好的辦法是使用臨時變數的時候,讓引用變數在推出活動域後自動設定為null,暗示垃圾收集器來收集該物件,防止發生記憶體洩漏。
2、程式進行字串處理時,儘量避免使用String,而應該使用StringBuffer。因為String類是不可變的,每一個String物件都會獨立佔用記憶體一塊區域。
3、儘量少用靜態變數。因為靜態變數是全域性的,存在方法區,GC不會回收。(用永久代實現的方法區,垃圾回收行為在這個區域是比較少出現的,垃圾回收器的主要目標是針對常量池和型別的解除安裝)
4、避免集中建立物件,尤其是大物件,如果可以的話儘量使用流操作。JVM會突然需要大量neicun,這時會出發GC優化系統記憶體環境
5、儘量運用物件池技術以提高系統性能。生命週期長的物件擁有生命週期短的物件時容易引發記憶體洩漏,例如大集合物件擁有大資料量的業務物件的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。
6、不要在經常呼叫的方法中建立物件,尤其忌諱在迴圈中建立物件。可以適當的使用hashtable,vector建立一組物件容器,然後從容器中去取這些物件,而不用每次new之後又丟棄。
7、優化配置 JVM調優(6)之引數配置

分析記憶體洩露

1、把Java應用程式使用的heap dump下來
2、使用Java heap分析工具,找出記憶體佔用超出預期(一般是因為數量太多)的嫌疑物件
3、必要時,需要分析嫌疑物件和其他物件的引用關係。
4、檢視程式的原始碼,找出嫌疑物件數量過多的原因。

dump heap

如果Java應用程式出現了記憶體洩露,千萬彆著急著把應用殺掉,而是要儲存現場。如果是網際網路應用,可以把流量切到其他伺服器。儲存現場的目的就是為了把執行中JVM的heap dump下來。
JDK自帶的jmap工具,可以做這件事情。它的執行方法是:

jmap -dump:format=b,file=heap.bin <pid>

format=b的含義是,dump出來的檔案時二進位制格式。
file-heap.bin的含義是,dump出來的檔名是heap.bin。
就是JVM的程序號。執行ps aux | grep java或者jps,找到JVM的pid;

JProfiler

JProfiler 是一個商業授權的Java剖析工具,由EJ技術有限公司,針對的Java EE和Java SE應用程式開發的。它把CPU、執行緒和記憶體的剖析組合在一個強大的應用中。JProfiler可提供許多IDE整合和應用伺服器整合用途。JProfiler的是一個獨立的應用程式,但其提供Eclipse和IntelliJ等IDE的外掛。它允許兩個記憶體剖面評估記憶體使用情況和動態分配洩漏和CPU剖析,以評估執行緒衝突。JProfiler直覺式的GUI讓你可以找到效能瓶頸、抓出記憶體漏失(memory leaks)、並解決執行緒的問題。它讓你得以對heap walker作資源回收器的root analysis,可以輕易找出記憶體漏失;heap快照(snapshot)模式讓未被參照(reference)的物件、稍微被參照的物件、或在終結(finalization)佇列的物件都會被移除;整合精靈以便剖析瀏覽器的Java外掛功能。
在這裡插入圖片描述
在這裡插入圖片描述