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() 包含如下幾個動作
- 右邊的 new Person 在堆空間分配一塊內存,創建一個 Person 類對象
- 末尾的 () 意味著創建對象之后,立即調用構造函數,進行初始化
- 左邊的 Person p1 創建了一個引用變量,所謂引用變量,就是后來用于指向 Person 類示例的引用
- = 符號使剛剛創建的對象引用指向剛剛創建的對象
上面的代碼如下所示:
如果再將對象賦值給 p2 的話,變成下面這樣的
執行 p2 = new Person() 之后變成
1.3 虛擬機垃圾自動回收機制
垃圾自動回收做兩件事情:
- 標記垃圾
- 清除垃圾
標記過程現在主要使用 根可達性 分析(還有引用計數法等),清除之后可能會有一些小的內存快,所有還有壓縮的過程。
下圖中的灰色對象表示可以被回收的對象(根不可達)
哪些對象可以成為 根 呢? 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-內存泄漏分析和對內存設置/