一、堆直方圖
減少內存使用時一個重要目標,在堆分析上最簡單的方法是利用堆直方圖。通過堆直方圖我們可以快速看到應用內的對象數目,同時不需要進行完整的堆轉儲(因為堆轉儲需要一段時間來分析,而且會消耗大量磁盤空間)。
直方圖擅長識別由分配了一兩個特定類的過多實例所引發的問題。例如應用中的內存壓力是由一些特定的對象類型引起的,利用堆直方圖可以很快就能看出端倪。
1.1、通過jcmd獲得
堆直方圖可以通過jcmd命令獲得:
[ciadmin@2-103test_app ~]$ jcmd 26964 GC.class_histogram | more 26964: num #instances #bytes class name ---------------------------------------------- 1: 91488 21270064 [C 2: 9058 18963152 [B 3: 80620 2579840 java.util.concurrent.ConcurrentHashMap$Node 4: 24081 2119128 java.lang.reflect.Method 5: 86860 2084640 java.lang.String 6: 13013 1444264 java.lang.Class 7: 24376 1170048 org.aspectj.weaver.reflect.ShadowMatchImpl 8: 26822 1072880 java.util.LinkedHashMap$Entry 9: 553 921168 [Ljava.util.concurrent.ConcurrentHashMap$Node; 10: 15903 890568 java.util.LinkedHashMap 11: 12092 847832 [Ljava.util.HashMap$Node; 12: 307 829712 [J 13: 24376 780032 org.aspectj.weaver.patterns.ExposedState 14: 12621 718696 [Ljava.lang.Object; 15: 5433 686400 [I 16: 36341 581456 java.lang.Object 17: 17746 567872 java.util.HashMap$Node 18: 1267 476392 java.lang.Thread 19: 13207 422624 java.lang.ThreadLocal$ThreadLocalMap$Entry 20: 2516 270336 [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;
說明:
1、字段說明
- [C:字符數組
- [B:字節數組
- [Ljava.lang.Object:Object數組
2、GC.class_histogram輸出的僅包含活躍對象
1.2、通過jmap獲得
命令為:jmap -histo process_id
jmap的輸出中包含會被回收的對象(死對象)。要在看到直方圖之前強制執行一次Full GC,可以轉而運行下面的命令:
如下,在命令行中增加:live參數后,輸出的直方圖是Full GC之后的數據
[ciadmin@2-103test_app ~]$ jmap -histo:live 26964 | more num #instances #bytes class name ---------------------------------------------- 1: 91488 21270064 [C 2: 9058 18963152 [B 3: 80620 2579840 java.util.concurrent.ConcurrentHashMap$Node 4: 24081 2119128 java.lang.reflect.Method 5: 86860 2084640 java.lang.String 6: 13013 1444264 java.lang.Class 7: 24376 1170048 org.aspectj.weaver.reflect.ShadowMatchImpl 8: 26822 1072880 java.util.LinkedHashMap$Entry 9: 553 921168 [Ljava.util.concurrent.ConcurrentHashMap$Node; 10: 15903 890568 java.util.LinkedHashMap 11: 12092 847832 [Ljava.util.HashMap$Node; 12: 307 829712 [J 13: 24376 780032 org.aspectj.weaver.patterns.ExposedState 14: 12621 718696 [Ljava.lang.Object; 15: 5433 686400 [I 16: 36341 581456 java.lang.Object 17: 17749 567968 java.util.HashMap$Node 18: 1267 476392 java.lang.Thread 19: 13207 422624 java.lang.ThreadLocal$ThreadLocalMap$Entry 20: 2516 270336 [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry; 21: 8056 257792 java.util.LinkedList 22: 11664 247432 [Ljava.lang.Class; 23: 4397 187064 [Ljava.lang.String; 24: 1945 186720 org.springframework.beans.GenericTypeAwarePropertyDescriptor 25: 5631 180192 java.lang.ref.WeakReference 26: 3630 174240 java.util.HashMap
直方圖非常小,但獲取直方圖也需要幾秒時間。性能測試時需要注意它。
二、堆轉儲
直方圖擅長識別由分配了一兩個特定類的過多實例所引發的問題,但是要深度分析,就需要堆轉儲了。
2.1、使用jcmd進行堆轉儲
[ciadmin@2-103test_app pos-gateway-cloud]$ jcmd 26964 GC.heap_dump /home/ciadmin/pos-gateway-cloud/heap_dump.hprof 26964: Heap dump file created
2.2、使用jmap進行堆轉儲
[ciadmin@2-103test_app pos-gateway-cloud]$ jmap -dump:live,file=/home/ciadmin/pos-gateway-cloud/heap_dump2.hprof 26964 Dumping heap to /home/ciadmin/pos-gateway-cloud/heap_dump2.hprof ... Heap dump file created
jmap中包含live選項,會在堆轉儲前執行一次Full GC;jcmd默認就會這么做。如果因為某些原因,不希望包含其他對象(即死對象),可以在jcmd命令的最后加上-all。
2.3、自動堆轉儲
OutOfMemoryError是不可預料的,我們很難確定應該何時獲得堆轉儲。有幾個JVM標志可以起到幫助。
-XX:+HeapDumpOnOutOfMemoryError該標志默認為false,打開該標志,JVM會在拋出OutOfMemoryError時創建堆轉儲。
-XX:HeapDumpPath=<path>該標志知道了堆轉儲將被寫入的位置,默認為當前工作目錄下生產java_pid<pid>.hprof文件。
-XX:+HeapDumpAfterFullGC 這會在運行一次Full GC后生成一個堆轉儲文件。
-XX:+HeapDumpBeforeFullGC 這會在運行一次Full GC之前生成一個堆轉儲文件。
有的情況下,(入幫,因為執行了多次Full GC)會生成多個堆轉儲文件,這時JVM會在堆轉儲文件的名字上附加一個序號。
這兩條命令都會在指定目錄下創建一個命名為*.hprof的文件。生成之后,有很多工具可以打開該文件。以下是三個最常見的工具。
三、堆轉儲文件分析工具
jhat
這是最原始的分析工具,它會讀取堆轉儲文件,並運行一個小型的HTTP服務器,該服務器運行你通過一系列網易鏈接查看堆轉儲信息。
[ciadmin@2-103test_app pos-gateway-cloud]$ jhat heap_dump.hprof Reading from heap_dump.hprof... Dump file created Mon Mar 05 18:33:10 CST 2018 Snapshot read, resolving... Resolving 751016 objects... Chasing references, expect 150 dots...................................................................................................................................................... Eliminating duplicate references...................................................................................................................................................... Snapshot resolved. Started HTTP server on port 7000 Server is ready.
找一台帶瀏覽器的機器訪問它,http://ip:7000
更多信息見《九、jdk工具之jhat命令(Java Heap Analyse Tool)、jhat之一:對dump的結果在瀏覽器上展示》
jvisualvm
jvisualvm的監視(Monitor) 選項卡可以從一個運行中的程序獲得堆轉儲文件,也可以打開之前生成堆轉儲文件。
更多信息見《八、jdk工具之JvisualVM、JvisualVM之一--(visualVM介紹及性能分析示例)》
mat
開源的EclipseLink內存分析器工具(EclipseLink Memory Analyzer Tool,mat)可以加載一個或多個堆轉儲文件並執行分析。它可以生成報告,向我們建議可能存在問題的地方,也可以用於流量堆,並對堆執行類SQL的查詢。
特別指出的是:mat內置一功能:如果打開了兩個堆轉儲文件,mat有一個選項用來計算兩個堆中的直方圖之間的差別。
更多信息見《mat之一--eclipse安裝Memory Analyzer》
對堆的第一遍分析通常涉及保留內存。一個對象的保留內存,是指回收該對象可以釋放出的內存量。
關於保留內存相關知識見《GC之二--GC是如何回收時的判斷依據、shallow(淺) size、retained(保留) size、Deep(深)size》
四、內存溢出錯誤
在下面情況下,jvm會拋出內存溢出錯誤(OutOfMemeoryError):
- JVM沒有原生內存可用;
- 永久代(在java7和更早的版本中)或元空間(java8)內存不足;
- java堆本身內存不足--對於給定的堆空間而言,應用中活躍對象太多;
- JVM執行GC耗時太多;
1、原生內存不足
其原因與堆根本無關。在32位的JVM中,一個進程的最大內存是4GB,如果指定一個非常大的堆大小,比如說3.8GB,使應用的大小很接近4GB的限制,這很危險。
2、永久代或元空間內存不足
首先與堆無關,其根源可能有兩種情況:
- 第一種情況是應用使用的類太多,超出了永久代的默認容納范圍;解決方案:增加永久代的大小
- 第二種情況相對來說有點棘手:它涉及類加載器的內存泄漏。這種情況經常出現於Java EE應用服務器中。類加載導致內存溢出可以通過堆轉儲分析,在直方圖中,找到ClassLoader類的所有實例,然后跟蹤他們的GC根,看哪些對象還保留了對它們的引用。
示例:

3、堆內存不足
當確實是堆內存本身不足時,錯誤信息會這樣:
OutOfMemoryError:Java heap space
可能的原因有:
1、活躍對象數目在為其配置的堆空間中已經裝不下了。
2、也可能是應用存在內存泄漏:它持續分配新對象,卻沒有讓其他對象退出作用域。
不管哪種情況,要找出哪些對象消耗的內存最多,堆轉儲分析都是必要的;
4、達到GC的開銷限制
JVM拋出OutOfMemoryError的最后一種情況是JVM任務在執行GC上花費了太多時間:
OutOfMemoryError:GC overhead limit exceeded
當滿足下列所有條件時就會拋出該錯誤:
1、花在Full GC的時間超出了-XX:GCTimeLimit=N標志指定的值。默認為98
2、一次Full GC回收內存量少於-XX:GCHeapFreeLimit=N標志指定的值。默認值為2(2%)
3、上面兩個條件連續5次Full GC都成立(這個值無法調整)
4、-XX:+UseGCOverhead-Limit標志為true(默認也為true)
這四個條件必須都滿足。一般來說,連續5次Full GC以上,不一定會拋異常。即使98%時間在Full GC上,但每次GC期間釋放的堆空間會超過2%,這種情況下可以增加-XX:GCHeapFreeLimit的值。
還請注意,如果前兩個條件連續4次Full GC周期都成立,作為釋放內存的最后一搏,JVM中所有的軟引用都會在第五次Full GC之前被釋放。這往往會防止該錯誤,因為第五次Full GC很可能會釋放超過2%的堆內存(假設該應用使用了軟引用)。
