Java 內存泄漏分析和對內存設置

goodboy 6年前發布 | 38K 次閱讀 JVM Java開發

為了判斷 Java 中是否有內存泄漏,我們首先必須了解 Java 是如何管理內存的。下面我們先給出一個簡單的內存泄漏的例子,在這個例子中我們循環申請 Object 對象,并將所申請的對象放入一個 HashMap 中,如果我們僅僅釋放引用本身,那么 HashMap 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。

HashMap mapObj = new HashMap()

    public void myfun() {
        String obj1 = new String("abcd");
        mapObj.put(obj1, obj1);
        ...
        obj1 = null;   //此時 obj1 指向的物理內存沒有釋放,因為 hashmap 引用該地址
    }

JVM 可以自動回收垃圾,但它只能回收滿足條件的垃圾,有時需要們確保條件的滿足。如果程序中,存在越來越多不在影響程序未來執行的對象(也就是不再需要的對象),而且這些對象和根對象之間存在引用路徑,那么就發生了內存泄漏。

內存泄漏常發生在如下場景:

  • 全局容器類,對象不再需要時,忘記從容器中 remove
  • 像 Runnable 對象等被 Java 虛擬機自身管理的對象,沒有正確的釋放渠道。Runnable 對象必須交給一個 Thread 去 run,否則該對象就永遠不會消亡

1.1 Java 對象的 Size

在 64 位的平臺上,Java 對象的占用內存如下

類型 大小
Object 16
Float 16
Double 24
Integer 16
Long 24

1.2 對象及其引用

為了說明對象和引用,我們先定義一個簡單的類

class Person {
    String name;
    int age;
}

Person p1 = new Person() 包含如下幾個動作

  1. 右邊的 new Person 在堆空間分配一塊內存,創建一個 Person 類對象
  2. 末尾的 () 意味著創建對象之后,立即調用構造函數,進行初始化
  3. 左邊的 Person p1 創建了一個引用變量,所謂引用變量,就是后來用于指向 Person 類示例的引用
  4. = 符號使剛剛創建的對象引用指向剛剛創建的對象
    上面的代碼如下所示:

如果再將對象賦值給 p2 的話,變成下面這樣的

執行 p2 = new Person() 之后變成

1.3 虛擬機垃圾自動回收機制

垃圾自動回收做兩件事情:

  1. 標記垃圾
  2. 清除垃圾

標記過程現在主要使用 根可達性 分析(還有引用計數法等),清除之后可能會有一些小的內存快,所有還有壓縮的過程。

下圖中的灰色對象表示可以被回收的對象(根不可達)

哪些對象可以成為 根 呢? http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3

  • 沒有被任何外部對象引用的棧上的對象
  • 靜態變量
  • JNI handler 包括全局和局部
  • 系統 Class
  • 存活著的監視器

2 內存泄漏的癥狀

2.1 為什么會發生 OOM 問題?

內存不足會有三種情況:

  • 對內存不足
  • 本地內存不足
  • Perm 內存不足

發生 OOM 的時候,可以檢查如下幾個方面:

  • 應用程序的緩存功能
  • 大量長期活動對象
  • 對內存泄漏
  • 本地內存泄漏

2.2 內存泄漏的癥狀

內存泄漏一般會有如下幾個癥狀:

  • 系統越來越慢,并且有 CPU 使用率過高
  • 運行一段時間后,OOM
  • 虛擬機 core dump

3 內存泄漏的定位和分析

內存泄漏的分析并不復雜,但需要耐心,一般內存泄漏只能事后分析,而重現問題需要耐心。

3.1 對內存泄漏定位

當出現 java.lang.OutOfMemoryError: Java Heap Space 異常,就表示堆內存不足了。堆內存不足的原因有如下幾種:

  • 堆內存設置太小
  • 內存泄漏
  • 設計不足,緩存了多余的數據
  • 如果懷疑有內存泄漏,可以添加 -verbose:gc 參數后重現啟動 Java 進程,輸出大致如下:
8190.813: [GC 164675K->251016K(1277056K), 0.0117749 secs] #8190.813 表示垃圾回收的時間點,秒為單位。GC/Full GC 表示垃圾回收的類型
8190.825: [Full GC 251016K->164654K(1277056K), 0.8142190 secs]   # 251016K表示回收前占用的內存大小,164654K 表示回收后占用的內存大小,1277056K 表示當前對內存總大小,0.8142190 表示回收耗時
8191.644: [GC 164678K->251214K(1277248K), 0.0123627 secs]
8191.657: [Full GC 251214K->164661K(1277248K), 0.8135393 secs]
8192.478: [GC 164700K->251285K(1277376K), 0.0130357 secs]
8192.491: [Full GC 251285K->164670K(1277376K), 0.8118171 secs]
8193.311: [GC 164726K->251182K(1277568K), 0.0121369 secs]
8193.323 : [Full GC 251182K->164644K(1277568K), 0.8186925 secs]
8194.156: [GC 164766K->251028K(1277760K), 0.0123415 secs]
8194.169: [Full GC 251028K->164660K(1277760K), 0.8144430 secs]

懷疑內存泄漏后,我們通過 Full GC 日志進一步確認,檢查 Full GC 后的可用內存是否持續增大。步驟如下:

  • 獲取系統穩定后的 GC 日志(不穩定的日志不可靠)
  • 過濾 FullGC 日志,可能會有如下兩種情況
    • FullGC 后內存使用量持續增長,一直到設置的堆內存最大值,基本可以確定內存泄漏
    • 內存使用量增長后又回落,出于一個動態平衡區間,基本排除內存泄漏

GC 日志只能幫忙找到是否有泄漏,找出內存泄漏的地方,需要依賴一些其他的工具

  • JProfile
  • OptimizedIt
  • JProbe
  • JConsole
  • -Xrunhprof

3.2 本地內存泄漏的定位

GC 日志無異常,但 Java 進程使用內存逐漸增大,并且無停止上漲的趨勢。本地內存泄漏的原因有如下幾個:

  • JNI 調用中出現內存泄漏(JNI 調用出現內存泄漏,可以使用 C/C++ 內存泄漏分析方法定位)
  • JDK bug
  • 操作系統問題

本地內存泄漏可能伴有如下異常

java.lang.OutOfMemoryError: unable to create new native thread , Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
      at java.lang.Thread.start0(Native Method)
      at java.lang.Thread.start(Thread.java:574)
      at TestThread.main(TestThread.java:34)

上面這個異常可能的原因有:

  • 創建的線程過多,可打印總線程數查看
  • swap 分區不足
  • 堆內存過大,本地內存不足

3.3 Perm 區內存不足定位

出現 java.lang.OutOfMemoryError: PermGen space Perm ,說明 Perm 區內存不足

  • 依賴注入,沒有卸載
  • Perm 區太小

 

來自:http://www.klion26.com/2018/03/14/Java-內存泄漏分析和對內存設置/

 

 本文由用戶 goodboy 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!