排查Java的記憶體問題

weixin_33763244發表於2018-03-13
\

核心要點

\\
  • 排查Java的記憶體問題可能會非常困難,但是正確的方法和適當的工具能夠極大地簡化這一過程;\\t
  • Java HotSpot JVM會報告各種OutOfMemoryError資訊,清晰地理解這些錯誤資訊非常重要,在我們的工具箱中有各種診斷和排查問題的工具,它們能夠幫助我們診斷並找到這些問題的根本原因;\\t
  • 在本文中,我們會介紹各種診斷工具,在解決記憶體問題的時候,它們是非常有用的,包括:\\t
    • HeapDumpOnOutOfMemoryError和PrintClassHistogram JVM選項\\t\t
    • Eclipse MAT\\t\t
    • Java VisualVM\\t\t
    • JConsole\\t\t
    • jhat\\t\t
    • YourKit\\t\t
    • jmap\\t\t
    • jcmd\\t\t
    • Java Flight Recorder和Java Mission Control\\t\t
    • GC Logs\\t\t
    • NMT\\t\t
    • 原生記憶體洩露探測工具,比如dbx、libumem、valgrind和purify等。\\t
    \
\\

對於一個Java程式來說,會有多個記憶體池或空間——Java堆、Metaspace、PermGen(在Java 8之前的版本中)以及原生堆。

\\

每個記憶體池都可能會遇到自己的記憶體問題,比如不正常的記憶體增加、應用變慢或者記憶體洩露,每種形式的問題最終都會以各自空間OutOfMemoryError的形式體現出來。

\\

在本文中,我們會嘗試理解這些OutOfMemoryError錯誤資訊的含義以及分析和解決這些問題要收集哪些診斷資料,另外還會研究一些用來收集和分析資料的工具,它們有助於解決這些記憶體問題。本文的關注點在於如何處理這些記憶體問題以及如何在生產環境中避免出現這些問題。

\\

Java HotSpot VM所報告的OutOfMemoryError資訊能夠清楚地表明哪塊記憶體區域正在耗盡。接下來,讓我們仔細看一下各種OutOfMemoryError資訊,理解其含義並探索導致它們出現的原因,最後介紹如何排查和解決這些問題。

\\

OutOfMemoryError: Java Heap Space

\\
\Exception in thread \"main\" java.lang.OutOfMemoryError: Java heap space\at java.util.Arrays.copyOfRange(Unknown Source)\at java.lang.String.\u0026lt;init\u0026gt;(Unknown Source)\at java.io.BufferedReader.readLine(Unknown Source)\at java.io.BufferedReader.readLine(Unknown Source)\at com.abc.ABCParser.dump(ABCParser.java:23)\at com.abc.ABCParser.mainABCParser.java:59)
\\

這個資訊表示JVM在Java堆上已經沒有空閒的空間,JVM無法繼續執行程式了。這種錯誤最常見的原因就是指定的最大Java堆空間已經不足以容納所有的存活物件了。要檢查Java堆空間是否足以容納JVM中所有存活的物件,一種簡單的方式就是檢查GC日誌。

\\
\688995.775: [Full GC [PSYoungGen: 46400K-\u0026gt;0K(471552K)] [ParOldGen: 1002121K-\u0026gt;304673K(1036288K)] 1048\521K-\u0026gt;304673K(1507840K) [PSPermGen: 253230K-\u0026gt;253230K(1048576K)], 0.3402350 secs] [Times: user=1.48 \sys=0.00, real=0.34 secs]\
\\

從上面的日誌條目我們可以看到在Full GC之後,堆的佔用從1GB(1048521K)降低到了305MB(304673K),這意味著分配給堆的1.5GB(1507840K)足以容納存活的資料集。

\\

現在,我們看一下如下的GC活動:

\\
\20.343: [Full GC (Ergonomics) [PSYoungGen: 12799K-\u0026gt;12799K(14848K)] [ParOldGen: 33905K-\u0026gt;33905K(34304K)] 46705K- \u0026gt;46705K(49152K), [Metaspace: 2921K-\u0026gt;2921K(1056768K)], 0.4595734 secs] [Times: user=1.17 sys=0.00, real=0.46 secs]\...... \u0026lt;snip\u0026gt; several Full GCs \u0026lt;/snip\u0026gt; ......\22.640: [Full GC (Ergonomics) [PSYoungGen: 12799K-\u0026gt;12799K(14848K)] [ParOldGen: 33911K-\u0026gt;33911K(34304K)] 46711K- \u0026gt;46711K(49152K), [Metaspace: 2921K-\u0026gt;2921K(1056768K)], 0.4648764 secs] [Times: user=1.11 sys=0.00, real=0.46 secs]\23.108: [Full GC (Ergonomics) [PSYoungGen: 12799K-\u0026gt;12799K(14848K)] [ParOldGen: 33913K-\u0026gt;33913K(34304K)] 46713K- \u0026gt;46713K(49152K), [Metaspace: 2921K-\u0026gt;2921 K(1056768K)], 0.4380009 secs] [Times: user=1.05 sys=0.00, real=0.44 secs]\23.550: [Full GC (Ergonomics) [PSYoungGen: 12799K-\u0026gt;12799K(14848K)] [ParOldGen: 33914K-\u0026gt;33914K(34304K)] 46714K- \u0026gt;46714K(49152K), [Metaspace: 2921K-\u0026gt;2921 K(1056768K)], 0.4767477 secs] [Times: user=1.15 sys=0.00, real=0.48 secs]\24.029: [Full GC (Ergonomics) [PSYoungGen: 12799K-\u0026gt;12799K(14848K)] [ParOldGen: 33915K-\u0026gt;33915K(34304K)] 46715K- \u0026gt;46715K(49152K), [Metaspace: 2921K-\u0026gt;2921 K(1056768K)], 0.4191135 secs] [Times: user=1.12 sys=0.00, real=0.42 secs] Exception in thread \"main\" java.lang.OutOfMemoryError: GC overhead limit exceeded at oom.main(oom.java:15)
\\

從轉儲的“Full GC”頻率資訊我們可以看到,這裡存在多次連續的Full GC,它會試圖回收Java堆中的空間,但是堆已經完全滿了,GC並沒有釋放任何空間。這種頻率的Full GC會對應用的效能帶來負面的影響,會讓應用變慢。這個樣例表明應用所需的堆超出了指定的Java堆的大小。增加堆的大小會有助於避免full GC並且能夠規避OutOfMemoryError。Java堆的大小可以通過-Xmx JVM選項來指定:

\\

java –Xmx1024m –Xms1024m Test

\\

OutOfMemoryError可能也是應用存在記憶體洩露的一個標誌。記憶體洩露通常難以察覺,尤其是緩慢的記憶體洩露。如果應用無意間持有了堆中物件的引用,會造成記憶體的洩露,這會導致物件無法被垃圾回收。隨著時間的推移,在堆中這些無意被持有的物件可能會隨之增加,最終填滿整個Java堆空間,導致頻繁的垃圾收集,最終程式會因為OutOfMemoryError錯誤而終止。

\\

請注意,最好始終啟用GC日誌,即便在生產環境也如此,在出現記憶體問題時,這樣有助於探測和排查。如下的選項能夠用來開啟GC日誌:

\\
\-XX:+PrintGCDetails\-XX:+PrintGCTimeStamps\-XX:+PrintGCDateStamps\-Xloggc:\u0026lt;gc log file\u0026gt;
\\

探測記憶體洩露的第一步就是監控應用的存活集合(live-set)。存活集合指的是full GC之後的Java堆。如果應用達到穩定狀態和穩定負載之後,存活集合依然在不斷增長,這表明可能會存在記憶體洩露。堆的使用情況可以通過Java VisualVM、Java Mission Control和JConsole這樣的工具來進行監控,也可以從GC日誌中進行抽取。

\\

Java堆:診斷資料的收集

\\

在這一部分中,我們將會討論要收集哪些診斷資料以解決Java堆上的OutOfMemoryErrors問題,有些工具能夠幫助我們收集所需的診斷資料。

\\

堆轉儲

\\

在解決記憶體洩露問題時,堆轉儲(dump)是最為重要的資料。堆轉儲可以通過jcmd、jmap、JConsole和HeapDumpOnOutOfMemoryError JVM配置項來收集,如下所示:

\\
  • jcmd \u0026lt;process id/main class\u0026gt; GC.heap_dump filename=heapdump.dmp\\t
  • jmap -dump:format=b,file=snapshot.jmap pid\\t
  • JConsole工具,使用Mbean HotSpotDiagnostic\\t
  • -XX:+HeapDumpOnOutOfMemoryError\
\java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xmx20m -XX:+HeapDumpOnOutOfMemoryError oom \0.402: [GC (Allocation Failure) [PSYoungGen: 5564K-\u0026gt;489K(6144K)] 5564K-\u0026gt;3944K(19968K), 0.0196154 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] \0.435: [GC (Allocation Failure) [PSYoungGen: 6000K-\u0026gt;496K(6144K)] 9456K-\u0026gt;8729K(19968K), 0.0257773 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] \0.469: [GC (Allocation Failure) [PSYoungGen: 5760K-\u0026gt;512K(6144K)] 13994K-\u0026gt;13965K(19968K), 0.0282133 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] \0.499: [Full GC (Ergonomics) [PSYoungGen: 512K-\u0026gt;0K(6144K)] [ParOldGen: 13453K-\u0026gt;12173K(13824K)] 13965K- \\u0026gt;12173K(19968K), [Metaspace: 2922K-\u0026gt;2922K(1056768K)], 0.6941054 secs] [Times: user=1.45 sys=0.00, real=0.69 secs] 1.205: [Full GC (Ergonomics) [PSYoungGen: 5632K-\u0026gt;2559K(6144K)] [ParOldGen: 12173K-\u0026gt;13369K(13824K)] 17805K- \\u0026gt;15929K(19968K), [Metaspace: 2922K-\u0026gt;2922K(1056768K)], 0.3933345 secs] [Times: user=0.69 sys=0.00, real=0.39 secs] \1.606: [Full GC (Ergonomics) [PSYoungGen: 4773K-\u0026gt;4743K(6144K)] [ParOldGen: 13369K-\u0026gt;13369K(13824K)] 18143K- \\u0026gt;18113K(19968K), [Metaspace: 2922K-\u0026gt;2922K(1056768K)], 0.3009828 secs] [Times: user=0.72 sys=0.00, real=0.30 secs] \1.911: [Full GC (Allocation Failure) [PSYoungGen: 4743K-\u0026gt;4743K(6144K)] [ParOldGen: 13369K-\u0026gt;13357K(13824K)] 18113K- \\u0026gt;18101K(19968K), [Metaspace: 2922K-\u0026gt;2922K(1056768K)], 0.6486744 secs] [Times: user=1.43 sys=0.00, real=0.65 secs] \java.lang.OutOfMemoryError: Java heap space \Dumping heap to java_pid26504.hprof ... \Heap dump file created [30451751 bytes in 0.510 secs] Exception in thread \"main\" java.lang.OutOfMemoryError: Java heap space\\ at java.util.Arrays.copyOf(Arrays.java:3210)\ at java.util.Arrays.copyOf(Arrays.java:3181)\ at java.util.ArrayList.grow(ArrayList.java:261)\ at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)\ at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)\ at java.util.ArrayList.add(ArrayList.java:458)\ at oom.main(oom.java:14)\
\\

請注意,並行垃圾收集器可能會連續地呼叫Full GC以便於釋放堆上的空間,即便這種嘗試的收益很小、堆空間幾乎已被充滿時,它可能也會這樣做。為了避免這種情況的發生,我們可以調節-XX:GCTimeLimit-XX:GCHeapFreeLimit的值。

\\

GCTimeLimit能夠設定一個上限,指定GC時間所佔總時間的百分比。它的預設值是98%。減少這個值會降低垃圾收集所允許花費的時間。GCHeapFreeLimit設定了一個下限,它指定了垃圾收集後應該有多大的空閒區域,這是一個相對於堆的總小大的百分比。它的預設值是2%。增加這個值意味著在GC後要回收更大的堆空間。如果五次連續的Full GC都不能保持GC的成本低於GCTimeLimit並且無法釋放 GCHeapFreeLimit所要求的空間的話,將會丟擲OutOfMemoryError

\\

例如,將GCHeapFreeLimit設定為8%的話,如果連續五次垃圾收集無法回收至少8%的堆空間並且超出了GCTimeLimit設定的值,這樣能夠幫助垃圾收集器避免連續呼叫Full GC的情況出現。

\\

堆直方圖

\\

有時,我們需要快速檢視堆中不斷增長的內容是什麼,繞過使用記憶體分析工具收集和分析堆轉儲的漫長處理路徑。堆直方圖能夠為我們快速展現堆中的物件,並對比這些直方圖,幫助我們找到Java堆中增長最快的是哪些物件。

\\
  • -XX:+PrintClassHistogram以及Control+Break\\t
  • jcmd \u0026lt;process id/main class\u0026gt; GC.class_histogram filename=Myheaphistogram\\t
  • jmap -histo pid\\t
  • jmap -histo \u0026lt;java\u0026gt; core_file\

下面的示例輸出顯示String、Double、Integer和Object[]的例項佔據了Java堆中大多數的空間,並且隨著時間的流逝數量在不斷增長,這意味著它們可能會導致記憶體洩露:

\\

1eef1dc8a019add92b8824df5d0ce9b6.png

\\

1b140d671a5696a45357df4fa3c44344.png

\\

Java飛行記錄

\\

將飛行記錄(Flight Recordings)啟用堆分析功能能夠幫助我們解決記憶體洩露的問題,它會展現堆中的物件以及隨著時間推移,哪些物件增長最快。要啟用堆分析功能,你可以使用Java Mission Control並選中“Heap Statistics”,這個選項可以通過“Window-\u0026gt;Flight Recording Template Manager”找到,如下所示:

\\

4ac31311a75e270b50746b16518515c1.jpg

\\

或者手動編輯.jfc檔案,將heap-statistics-enabled設定為true。

\\
\\u0026lt;event path=\"vm/gc/detailed/object_count\"\u0026gt;\    \u0026lt;setting name=\"enabled\" control=\"heap-statistics-enabled\"\u0026gt;true\u0026lt;/setting\u0026gt;\    \u0026lt;setting name=\"period\"\u0026gt;everyChunk\u0026lt;/setting\u0026gt;\\u0026lt;/event\u0026gt;
\\

飛行記錄可以通過如下的方式來建立:

\\
  • JVM Flight Recorder選項,比如:\

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
\-XX:StartFlightRecording=delay=20s,duration=60s,name=MyRecording,

\\

filename=C:\\TEMP\\myrecording.jfr,settings=profile

\\
  • Java診斷命令:jcmd\

jcmd 7060 JFR.start name=MyRecording settings=profile delay=20s duration=2m filename=c:\\TEMP\\myrecording.jfr

\\
  • Java任務控制(Java Mission Control)\

飛行記錄器只能幫我們確定哪種型別的物件出現了洩露,但是想要找到是什麼原因導致了這些物件洩露,我們還需要堆轉儲。

\\

Java堆:分析診斷資料

\\

堆轉儲分析

\\

堆轉儲可以使用如下的工具進行分析:

\\
  • Eclipse MAT(記憶體分析工具,Memory Analyzer Tool)是一個社群開發的分析堆轉儲的工具。它提供了一些很棒的特性,包括:\\\t
    • 可疑的洩漏點:它能探測堆轉儲中可疑的洩露點,報告持續佔有大量記憶體的物件;\\t\t
    • 直方圖:列出每個類的物件數量、淺大小(shallow)以及這些物件所持有的堆。直方圖中的物件可以很容易地使用正規表示式進行排序和過濾。這樣有助於放大並集中我們懷疑存在洩露的物件。它還能夠對比兩個堆轉儲的直方圖,展示每個類在例項數量方面的差異。這樣能夠幫助我們查詢Java堆中增長最快的物件,並進一步探查確定在堆中持有這些物件的根;\\t\t
    • 不可達的物件:MAT有一個非常棒的功能,那就是它允許在它的工作集物件中包含或排除不可達/死物件。如果你不想檢視不可達的物件,也就是那些會在下一次GC週期中收集掉的物件,只關心可達的物件,那麼這個特性是非常便利的;\\t\t
    • 重複的類:展現由多個類載入器所載入的重複的類;\\t\t
    • 到GC根的路徑:能夠展示到GC根(JVM本身保持存活的物件)的引用鏈,這些GC根負責持有堆中的物件;\\t\t
    • OQL:我們可以使用物件查詢語言(Object Query Language)來探查堆轉儲中的物件。它豐富了OQL的基礎設施,能夠編寫複雜的查詢,幫助我們深入瞭解轉儲的內部。\\t
    \
  • Java VisualVM:監控、分析和排查Java語言的一站式工具。它可以作為JDK工具的一部分來使用,也可以從GitHub上下載。它所提供的特性之一就是堆轉儲分析。它能夠為正在監控的應用建立堆轉儲,也可以載入和解析它們。從堆轉儲中,它可以展現類的直方圖、類的例項,也能查詢特定例項的GC根;\\t
  • jhat命令工具(在\u0026lt;jdk\u0026gt;/bin資料夾中)提供了堆轉儲分析的功能,它能夠在任意的瀏覽器中展現堆轉儲中的物件。預設情況下,Web伺服器會在7000埠啟動。jhat支援範圍廣泛的預定義查詢和物件查詢語言,以便於探查堆轉儲中的物件;\\t
  • Java任務控制(Java Mission Control)的JOverflow外掛:這是一個實驗性的外掛,能夠讓Java任務控制執行簡單的堆轉儲分析並報告哪裡可能存在記憶體浪費;\\t
  • Yourkit是一個商業的Java profiler,它有一個堆轉儲分析器,具備其他工具所提供的幾乎所有特性。除此之外,YourKit還提供了:\\t
    • 可達性的範圍(reachability scope):它不僅能夠列出可達和不可達的物件,還能按照它們的可達性範圍顯示它們的分佈,也就是,強可達、弱/軟可達或不可達;\\t\t
    • 記憶體探查:YourKit內建了一組全面的查詢,而不是使用ad-hoc查詢功能,YourKit的查詢能夠探查記憶體,查詢反模式併為常見的記憶體問題分析產生原因和提供解決方案。\\t
    \

我使用Eclipse MAT較多,我發現在分析堆轉儲時,它是非常有用的。

\\

40129511d8dfd6d024cd0b0d36f2bebd.jpg

\\

MAT有一些高階的特性,包括直方圖以及與其他的直方圖進行對比的功能。這樣的話,就能清晰地看出記憶體中哪些內容在增長並且能夠看到Java堆中佔據空間最大的是什麼內容。我非常喜歡的一個特性是“Merge Shortest Paths to GC Roots(合併到GC Root的最短路徑)”,它能夠幫助我們查詢無意中所持有的物件的跟蹤痕跡。比如,在下面的引用鏈中,ThreadLocalDateFormat物件被ThreadLocalMap$Entry物件的“value”欄位所持有。只有當ThreadLocalMap$Entry從ThreadLocalMap中移除之後,ThreadLocalDateFormat才能被回收。

\\
\weblogic.work.ExecuteThread @ 0x6996963a8 [ACTIVE] ExecuteThread: '203' for queue: 'weblogic.kernel.Default (self-tuning)' Busy Monitor, Thread| 1 | 176 | 40 | 10,536\\'- threadLocals java.lang.ThreadLocal$ThreadLocalMap @ 0x69c2b5fe0 | 1 | 24 | 40 | 7,560\\'- table java.lang.ThreadLocal$ThreadLocalMap$Entry[256] @ 0x6a0de2e40 | 1 | 1,040 | 40 | 7,536\\'- [116] java.lang.ThreadLocal$ThreadLocalMap$Entry @ 0x69c2ba050 | 1 | 32 | 40 | 1,088\\'- value weblogic.utils.string.ThreadLocalDateFormat @ 0x69c23c418 | 1 | 40 | 40 | 1,056\
\\

通過這種方式,我們可以看到堆中增長最快的罪魁禍首,並且看到記憶體中哪裡出現了洩露。

\\

Java任務控制

\\

Java任務控制可以在JDK的\u0026lt;jdk\u0026gt;/bin資料夾中找到。啟用Heap Statistics功能之後所收集到的飛行記錄能夠極大地幫助我們解決記憶體洩露問題。我們可以在Memory-\u0026gt;Object Statistics中檢視物件的分析資訊。這個檢視將會展現物件的直方圖,包括每個物件型別所佔據的堆的百分比。它能夠展現堆中增長最快的物件,在大多數情況下,也就直接對應了記憶體洩露的物件。

\\

終結器所導致的OutOfMemoryError

\\

濫用終結器(finalizer)可能也會造成OutOfMemoryError。帶有終結器的物件(也就是含有finalize()方法)會延遲它們所佔有空間的回收。在回收這些例項並釋放其堆空間之前,終結器執行緒(finalizer thread)需要呼叫它們的finalize()方法。如果終結者執行緒的處理速度比不上要終結物件的增加速度(新增到終結者佇列中以便於呼叫其finalize()方法)的話,那麼即便終結器佇列中的物件都有資格進行回收,JVM可能也會出現OutOfMemoryError。因此,非常重要的一點就是確保不要因為大量物件等待(pending)終結而造成記憶體耗盡。

\\

我們可以使用如下的工具來監控等待終結的物件數量:

\\
  • JConsole\

我們可以連線JConsole到一個執行中的程式,然後在VM Summary頁面檢視等待終結的物件數量,如下圖所示。

\\

f16e0c04f2b4caadc4141594cc079728.jpg

\\
  • jmap – finalizerinfo\
\D:\\tests\\GC_WeakReferences\u0026gt;jmap -finalizerinfo 29456 \Attaching to process ID 29456, please wait...\Debugger attached successfully. Server compiler detected.\JVM version is 25.122-b08\Number of objects pending for finalization: 10\
\\
  • 堆轉儲\

幾乎所有的堆轉儲分析工具都能詳細給出等待終結的物件資訊。

\\

Java VisualVM的輸出

\\
\Date taken: Fri Jan 06 14:48:54 PST 2017\\tFile: D:\\tests\\java_pid19908.hprof\\tFile size: 11.3 MB\ \\tTotal bytes: 10,359,516\\tTotal classes: 466\\tTotal instances: 105,182\\tClassloaders: 2\\tGC roots: 419\\tNumber of objects pending for finalization: 2\
\\\\

java.lang.OutOfMemoryError: PermGen space

\\

我們知道,從Java 8之後,PermGen已經移除掉了。如果讀者執行的是Java 8以上的版本,那麼這一小節可以直接略過。

\\

在Java 7及以前,PermGen(“永久代,permanent generation”的縮寫)用來儲存類定義以及它們的後設資料。在這個記憶體區域中,PermGen意料之外的增長以及OutOfMemoryError意味著類沒有按照預期解除安裝,或者所指定的PermGen空間太小,無法容納所有要載入的類和它們的後設資料。

\\

要確保PermGen的大小能夠滿足應用的需求,我們需要監控它的使用情況並使用如下的JVM選項進行相應的配置:

\\

        –XX:PermSize=n –XX:MaxPermSize=m

\\\\

MetaSpace的OutOfMemoryError輸出樣例如下所示:

\\

java.lang.OutOfMemoryError: Metaspace

\\

從Java 8開始,類後設資料儲存到了Metaspace中。Metaspace並不是Java堆的一部分,它是分配在原生記憶體上的。所以,它僅僅受到機器可用原生記憶體數量的限制。但是,Metaspace也可以通過 MaxMetaspaceSize引數來設定它的大小。

\\

如果Metaspace的使用接近MaxMetaspaceSize的最大限制,那麼我們就會遇到OutOfMemoryError。與其他的區域類似,這種錯誤可能是因為沒有足夠的Metaspace,或者存在類載入器/類洩露。如果出現了後者的情況,我們需要藉助診斷工具,解決Metaspace中的記憶體洩露。

\\\\

java.lang.OutOfMemoryError: Compressed class space

\\

如果啟用了UseCompressedClassesPointers的話(開啟UseCompressedOops的話之後,會預設啟用),那麼原生記憶體上會有兩個獨立的區域用來儲存類和它們的後設資料。啟用UseCompressedClassesPointers之後,64位的類指標會使用32位的值來表示,壓縮的類指標會儲存在壓縮類空間(compressed class space)中。預設情況下,壓縮類空間的大小是1GB並且可以通過CompressedClassSpaceSize進行配置。

\\

MaxMetaspaceSize能夠為這兩個區域設定一個總的提交(committed)空間大小,即壓縮類空間和類後設資料的提交空間。

\\

啟用UseCompressedClassesPointers之後,在GC日誌中會進行取樣輸出。在Metaspace所報告的提交和保留(reserved)空間中包含了壓縮類空間的提交和預留空間。

\\
\Metaspace     used 2921K, capacity 4486K, committed 4864K, reserved 1056768K\  class space used 288K, capacity 386K, committed 512K, reserved 1048576K
\\

PermGen和Metaspace:資料收集和分析工具

\\

PermGen和Metaspace所佔據的空間可以使用Java任務控制、Java VisualVM和JConsole進行監控。GC能夠幫助我們理解Full GC前後PermGen/Metaspace的使用情況,也能看到是否存在因為PermGen/Metaspace充滿而導致的Full GC。

\\

另外非常重要的一點在於確保類按照預期進行了解除安裝。類的載入和解除安裝可以通過啟用下面的引數來進行跟蹤:

\\

-XX:+TraceClassUnloading –XX:+TraceClassLoading

\\

在將應用從開發環境提升到生產環境時,需要注意應用程式有可能會被無意地改變一些JVM可選引數,從而帶來不良的後果。其中有個選項就是-Xnoclassgc,它會讓JVM在垃圾收集的時候不去解除安裝類。現在,如果應用需要載入大量的類,或者在執行期有些類變得不可達了,需要載入另外一組新類,應用恰好是在–Xnoclassgc模式下執行的,那麼它有可能達到PermGen/Metaspace的最大容量,就會出現OutOfMemoryError。因此,如果你不確定這個選項為何要設定的話,那麼最好將其移除,讓垃圾收集器在這些類能夠回收的時候將其解除安裝掉。

\\

載入的類和它們所佔用的記憶體可以通過Native Memory Tracker(NMT)來進行跟蹤。我們將會在下面的“OutOfMemoryError: Native Memory”小節詳細討論這個工具。

\\

需要注意,在使用併發標記清除收集器(Concurrent MarkSweep Collector,CMS)時,需要啟用如下的選項,從而確保CMS併發收集週期能夠將類解除安裝掉:–XX:+CMSClassUnloadingEnabled

\\

在Java 7中,這個標記預設是關閉的,而在Java 8中它預設就是啟用的。

\\

jmap

\\

“jmap –permstat”會展現類載入器的統計資料,比如類載入器、類載入器所載入的類的數量以及這些類載入已死亡還是尚在存活。它還會告訴我們PermGen中interned字串的總數,以及所載入的類及其後設資料所佔用的位元組數。如果我們要確定是什麼內容佔滿了PermGen,那麼這些資訊是非常有用的。如下是一個示例的輸出,展現了所有的統計資訊。在列表的最後一行我們能夠看到有一個總數的概述。

\\
\$ jmap -permstat 29620\Attaching to process ID 29620, please wait...\Debugger attached successfully. Client compiler detected.\JVM version is 24.85-b06\12674 intern Strings occupying 1082616 bytes. finding class loader instances ..\ done. computing per loader stat ..done. please wait.. computing liveness.........................................done.\class_loader\tclasses bytes parent_loader   alive?  type\\u0026lt;bootstrap\u0026gt; 1846 5321080  null  live   \u0026lt;internal\u0026gt;\0xd0bf3828  0   0  \tnull   live    sun/misc/Launcher$ExtClassLoader@0xd8c98c78\0xd0d2f370  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0c99280  1   1440  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0b71d90  0   0   0xd0b5b9c0\tlive \t  java/util/ResourceBundle$RBClassLoader@0xd8d042e8\0xd0d2f4c0  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0b5bf98  1   920   0xd0b5bf38 dead   sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0c99248  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f488  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0b5bf38  6   11832  0xd0b5b9c0 dead  sun/reflect/misc/MethodUtil@0xd8e8e560\0xd0d2f338  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f418  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f3a8  1   904 \tnull   dead\t   sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0b5b9c0  317 1397448 0xd0bf3828 live sun/misc/Launcher$AppClassLoader@0xd8cb83d8\0xd0d2f300  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f3e0  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0ec3968  1   1440  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0e0a248  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0c99210  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f450  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0d2f4f8  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\0xd0e0a280  1   904  \tnull   dead    sun/reflect/DelegatingClassLoader@0xd8c22f50\ \total = 22  \t2186    6746816   N/A   alive=4, dead=18   \tN/A\
\\

從Java 8開始,jmap –clstats \u0026lt;pid\u0026gt;命令能夠列印類載入器及其存活性的類似資訊,不過它所展現的是Metaspace中已載入的類的數量和大小,而不再是PermGen。

\\
\jmap -clstats 26240\Attaching to process ID 26240, please wait...\Debugger attached successfully. Server compiler detected. JVM version is 25.66-b00 finding class loader instances ..done. computing per loader stat ..done. please wait.. computing liveness.liveness analysis may be inaccurate ...\class_loader\t classes bytes parent_loader alive? type\\u0026lt;bootstrap\u0026gt;        513 950353 null live \u0026lt;internal\u0026gt;\0x0000000084e066d0 8 24416  0x0000000084e06740 live sun/misc/Launcher$AppClassLoader@0x0000000016bef6a0\0x0000000084e06740 0 0      null live sun/misc/Launcher$ExtClassLoader@0x0000000016befa48\0x0000000084ea18f0 0 0 0x0000000084e066d0 dead java/util/ResourceBundle$RBClassLoader@0x0000000016c33930\ \total = 4   \t521 \t974769      N/A     \talive=3, dead=1 \tN/A\
\\

堆轉儲

\\

正如我們在前面的章節所提到的,Eclipse MAT、jhat、Java VisualVM、JOverflow JMC外掛和Yourkit這些工具都能分析堆轉儲檔案,從而分析排查OutOfMemoryError。在解決PermGen和Metaspace的記憶體問題時,堆轉儲同樣是有用的。Eclipse MAT提供了一個非常好的特性叫做“Duplicate Classes”,它能夠列出被不同的類載入例項多次載入的類。由不同的類載入器載入數量有限的重複類可能是應用設計的一部分,但是,如果它們的數量隨著時間推移不斷增長的話,那麼這就是一個危險的訊號,需要進行調查。應用伺服器託管多個應用時,它們執行在同一個JVM中,如果多次解除安裝和重新部署應用的話,經常會遇到這種狀況。如果被解除安裝的應用沒有釋放所有它建立的類載入器的引用,JVM就不能解除安裝這些類載入器所載入的類,而新部署的應用會使用新的類載入器例項重新載入這些類。

\\

180ba0a52b9e5e5ade80b3d2b73db5e1.jpg

\\

這個快照顯示JaxbClassLoader載入了類的重複副本,這是因為應用在為每個XML進行Java類繫結的時候,不恰當地建立了新的JAXBContext例項。

\\

jcmd

\\

jcmd \u0026lt;pid/classname\u0026gt; GC.class_stats能夠提供被載入類的更詳細資訊,藉助它,我們能夠看到Metaspace每個類所佔據的空間,如下面的示例輸出所示。

\\
\jcmd 2752 GC.class_stats 2752:\Index  Super  InstBytes  KlassBytes  annotations  CpAll  MethodCount  Bytecodes  MethodAll  ROAll   RWAll   Total  ClassName\1  \t357 \t821632 \t536       \t0      \t352 \t2       \t13     \t616    \t184 \t1448\t1632 java.lang.ref.WeakReference\2  \t-1  \t295272 \t480       \t0      \t0   \t0       \t0      \t0      \t24  \t584 \t608 [Ljava.lang.Object;\3  \t-1  \t214552 \t480       \t0      \t0   \t0       \t0      \t0      \t24  \t584 \t608 [C\4  \t-1  \t120400 \t480       \t0      \t0   \t0       \t0      \t0      \t24  \t584 \t608 [B\5  \t35  \t78912  \t624       \t0      \t8712\t94      \t4623   \t26032  \t12136   24312   36448 java.lang.String\6  \t35  \t67112  \t648       \t0      \t19384   130     \t4973   \t25536  \t16552   30792   47344 java.lang.Class\7  \t9   \t24680  \t560       \t0      \t384 \t1       \t10     \t496    \t232 \t1432\t1664 java.util.LinkedHashMap$Entry\8  \t-1  \t13216  \t480       \t0      \t0   \t0       \t0      \t0      \t48  \t584 \t632 [Ljava.lang.String;\9  \t35  \t12032  \t560       \t0      \t1296\t7       \t149    \t1520   \t880 \t2808\t3688 java.util.HashMap$Node\10 \t-1  \t8416   \t480       \t0      \t0   \t0       \t0      \t0      \t32  \t584 \t616 [Ljava.util.HashMap$Node;\11 \t-1  \t6512   \t480       \t0      \t0   \t0       \t0      \t0      \t24  \t584 \t608 [I\12 \t358 \t5688   \t720       \t0      \t5816\t44      \t1696   \t8808   \t5920\t10136   16056 java.lang.reflect.Field\13 \t319 \t4096   \t568       \t0      \t4464\t55      \t3260   \t11496  \t7696\t9664\t17360 java.lang.Integer\14 \t357 \t3840   \t536       \t0      \t584 \t3       \t56     \t496    \t344 \t1448\t1792 java.lang.ref.SoftReference\15 \t35  \t3840   \t584       \t0      \t1424\t8       \t240    \t1432   \t1048\t2712\t3760 java.util.Hashtable$Entry\16 \t35  \t2632   \t736       \t368    \t8512\t74      \t2015   \t13512  \t8784\t15592   24376 java.lang.Thread\17 \t35  \t2496   \t504       \t0      \t9016\t42      \t2766   \t9392   \t6984\t12736   19720 java.net.URL\18 \t35  \t2368   \t568       \t0      \t1344\t8       \t223    \t1408   \t1024\t2616\t3640 java.util.concurrent.ConcurrentHashMap$Node\…\u0026lt;snip\u0026gt;…\577\t35  \t0      \t544       \t0      \t1736\t3       \t136    \t616    \t640 \t2504\t3144 sun.util.locale.provider.SPILocaleProviderAdapter$1\578\t35  \t0      \t496       \t0      \t2736\t8       \t482    \t1688   \t1328\t3848\t5176 sun.util.locale.provider.TimeZoneNameUtility\579\t35  \t0      \t528       \t0      \t776 \t3       \t35     \t472    \t424 \t1608\t2032 sun.util.resources.LocaleData$1\580\t442 \t0      \t608       \t0      \t1704\t10      \t290    \t1808   \t1176\t3176\t4352 sun.util.resources.OpenListResourceBundle\581\t580 \t0      \t608       \t0      \t760 \t5       \t70     \t792    \t464 \t1848\t2312 sun.util.resources.TimeZoneNamesBundle\          \t1724488 \t357208    \t1536   \t1117792 7754    \t311937 \t1527952\t1014880 2181776 3196656 Total\            \t53.9%  \t11.2%    \t0.0%   \t35.0%\t-      \t9.8%   \t47.8%  \t31.7%   68.3%   100.0%\Index  Super  InstBytes  KlassBytes  annotations  CpAll  MethodCount  Bytecodes  MethodAll  ROAll   RWAll   Total  ClassName\
\\

從這個輸出中,我們可以看到所載入類的名稱(ClassName)、每個類所佔據的位元組(KlassBytes)、每個類的例項所佔據的位元組(InstBytes)、每個類中方法的數量(MethodCount)、位元組碼所佔據的空間(ByteCodes))等等。

\\

需要注意的是,在Java 8中,這個診斷命令需要Java程式使用‑XX:+UnlockDiagnosticVMOptions選項啟動。

\\
\jcmd 33984 GC.class_stats 33984:\GC.class_stats command requires -XX:+UnlockDiagnosticVMOptions\
\\

在Java 9中,該診斷命令不需要-XX:+UnlockDiagnosticVMOption。

\\\\

原生記憶體出現OutOfMemoryError的一些示例如下所示:
因為沒有足夠交換空間(swap space)所引起的OutOfMemoryError:

\\
\# A fatal error has been detected by the Java Runtime Environment:\ \#\# java.lang.OutOfMemoryError: requested 32756 bytes for ChunkPool::allocate. Out of swap space?\#\#  Internal Error (allocation.cpp:166), pid=2290, tid=27 #  Error: ChunkPool::allocate
\\

因為沒有足夠程式記憶體所導致的OutOfMemoryError:

\\
\# A fatal error has been detected by the Java Runtime Environment:\#\# java.lang.OutOfMemoryError : unable to create new native Thread
\\
\

這些錯誤清楚地表明JVM不能分配原生記憶體,這可能是因為程式本身消耗掉了所有的原生記憶體,也可能是系統中的其他程式在消耗原生記憶體。在使用“pmap”(或其他原生記憶體對映工具)監控原生堆的使用之後,我們可以恰當地配置Java堆、執行緒數以及棧的大小,確保有足夠的空間留給原生堆,如果我們發現原生堆的使用在持續增長,最終會出現OutOfMemoryError,那麼這可能提示我們遇到了原生記憶體的洩露。

\\

64位JVM上的原生堆OutOfMemoryError

\\

在32位JVM中,程式大小的上限是4GB,所以在32位Java程式中更容易出現原生記憶體耗盡的情況。但是,在64位JVM中,對記憶體的使用是沒有限制的,從技術上講,我們可能認為永遠也不會遇到原生堆耗盡的情況,但事實並非如此,原生堆遇到OutOfMemoryErrors的情況並不少見。這是因為64位JVM預設會啟用一個名為CompressedOops的特性,該特性的實現會決定要將Java堆放到地址空間的什麼位置。Java堆的位置可能會對原生記憶體的最大容量形成限制。在下面的記憶體地圖中,Java堆在8GB的地址邊界上進行了分配,剩下了大約4GB留給原生堆。如果應用需要在原生記憶體分配大量空間,超出了4GB的話,即便系統還有大量的記憶體可用,它依然會丟擲原生堆OutOfMemoryError。

\
\\
\0000000100000000 8K r-x-- /sw/.es-base/sparc/pkg/jdk-1.7.0_60/bin/sparcv9/java\0000000100100000 8K rwx-- /sw/.es-base/sparc/pkg/jdk-1.7.0_60/bin/sparcv9/java\0000000100102000 56K rwx--\t    [ heap ]\0000000100110000 2624K rwx--\t[ heap ]   \u0026lt;--- native Heap\00000001FB000000 24576K rw---\t[ anon ]   \u0026lt;--- Java Heap starts here\0000000200000000 1396736K rw---\t[ anon ]\0000000600000000 700416K rw---\t[ anon ]\
\\

這個問題可以通過-XX:HeapBaseMinAddress=n選項來解決,它能指定Java堆的起始地址。將它的設定成一個更高的地址將會為原生堆留出更多的空間。

\\

關於如何診斷、排查和解決該問題,請參閱該文瞭解更詳細的資訊。

\\

原生堆:診斷工具

\\

讓我們看一下記憶體洩露的探查工具,它們能夠幫助我們找到原生記憶體洩露的原因。

\\

原生記憶體跟蹤

\\

JVM有一個強大的特性叫做原生記憶體跟蹤(Native Memory Tracking,NMT),它在JVM內部用來跟蹤原生記憶體。需要注意的是,它無法跟蹤JVM之外或原生庫分配的記憶體。通過下面兩個簡單的步驟,我們就可以監控JVM的原生記憶體使用情況:

\\
  • 以啟用NMT的方式啟動程式。輸出級別可以設定為“summary”或“detail”級別:\\t
    • -XX:NativeMemoryTracking=summary\\t\t
    • -XX:NativeMemoryTracking=detail\\t
    \\t
  • 使用jcmd來獲取原生記憶體使用的細節:\\t
    • jcmd \u0026lt;pid\u0026gt; VM.native_memory  \\t
    \

NMT輸出的樣例:

\\
\d:\\tests\u0026gt;jcmd 90172 VM.native_memory  90172:\Native Memory Tracking:\Total: reserved=3431296KB, committed=2132244KB\-                 Java Heap (reserved=2017280KB, committed=2017280KB)\            (mmap: reserved=2017280KB, committed=2017280KB)\-                 Class (reserved=1062088KB, committed=10184KB)\            (classes #411)\            (malloc=5320KB #190)\            (mmap: reserved=1056768KB, committed=4864KB)\-                  Thread (reserved=15423KB, committed=15423KB)\            (thread #16)\            (stack: reserved=15360KB, committed=15360KB)\            (malloc=45KB #81)\            (arena=18KB #30)\-                 Code (reserved=249658KB, committed=2594KB)\            (malloc=58KB #348)\            (mmap: reserved=249600KB, committed=2536KB)\-                 GC (reserved=79628KB, committed=79544KB)\            (malloc=5772KB #118)\            (mmap: reserved=73856KB, committed=73772KB)\-                 Compiler (reserved=138KB, committed=138KB)\            (malloc=8KB #41)\            (arena=131KB #3)\-                 Internal (reserved=5380KB, committed=5380KB)\            (malloc=5316KB #1357)\            (mmap: reserved=64KB, committed=64KB)\-                 Symbol (reserved=1367KB, committed=1367KB)\            (malloc=911KB #112)\            (arena=456KB #1)\-                 Native Memory Tracking (reserved=118KB, committed=118KB)\            (malloc=66KB #1040)\            (tracking overhead=52KB)\-                 Arena Chunk (reserved=217KB, committed=217KB)\            (malloc=217KB)\
\\

關於使用jcmd命令訪問NMT資料的細節以及如何閱讀它的輸出,可以參見該文

\\

原生記憶體洩露探查工具

\\

對於JVM外部的原生記憶體洩露,我們需要依賴原生記憶體洩露工具來進行探查和解決。原生工具能夠幫助我們解決JVM之外的原生記憶體洩露問題,這樣的工具包括dbxlibumemvalgrind以及purify等。

\\

總結

\\

排查記憶體問題可能會非常困難和棘手,但是正確的方法和適當的工具能夠極大地簡化這一過程。我們看到,Java HotSpot JVM會報告各種OutOfMemoryError資訊,清晰地理解這些錯誤資訊非常重要,在工具集中有各種診斷和排查工具,幫助我們診斷和根治這些問題。

\\

關於作者

\\

25e231982f8f1953bf33576a72150dea.jpgPoonam Parhar, 目前在Oracle擔任JVM支援工程師,她的主要職責是解決客戶針對JRockit和HotSpot JVM的問題。她樂於除錯和解決問題,主要關注於如何提升JVM的可服務性(serviceability)和可支援性(supportability)。她解決過HotSpot JVM中很多複雜的問題,熱衷於改善除錯工具和產品的可服務性,從而更容易地排查和定位JVM中垃圾收集器相關的問題。在幫助客戶和Java社群的過程中,她通過部落格分享了她的工作經驗和知識。

\\

檢視英文原文:Troubleshooting Memory Issues in Java Applications

相關文章