1. 前言
熟練掌握 MAT 是 Java 高手的必備能力,但實踐時大家往往需面對眾多功能,眼花繚亂不知如何下手,小編也沒有找到一篇完善的教學素材,所以整理本文幫大家系統(tǒng)掌握 MAT 分析工具。
本文詳細講解 MAT 眾多內(nèi)存分析工具功能,這些功能組合使用異常強大,熟練使用幾乎可以解決所有的堆內(nèi)存離線分析的問題。我們將功能劃分為4類:內(nèi)存分布詳情、對象間依賴、對象狀態(tài)詳情、按條件檢索。每大類有多個功能點,本文會逐一講解各功能的場景及用法。此外,添加了原創(chuàng)或引用案例加強理解和掌握。
如圖所示:
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
為減少對眼花繚亂的菜單的迷茫,可以通過下圖先整體熟悉下各功能使用入口,后續(xù)都會講到。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
2. 內(nèi)存分布詳解及實戰(zhàn)
2.1 全局信息概覽
功能:展現(xiàn)堆內(nèi)存大小、對象數(shù)量、class 數(shù)量、class loader 數(shù)量、GC Root 數(shù)量、環(huán)境變量、線程概況等全局統(tǒng)計信息。
使用入口:MAT 主界面 → Heap Dump Overview。
舉例:下面是對象數(shù)量、class loader 數(shù)量、GC Root 數(shù)量,可以看出 class loader 存在異常。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
舉例:下圖是線程概況,可以查看每個線程名、線程的 Retained Heap、daemon 屬性等。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用場景 全局概覽呈現(xiàn)全局統(tǒng)計信息,重點查看整體是否有異常數(shù)據(jù),所以有效信息有限,下面幾種場景有一定幫助:
-
方法區(qū)溢出時(Java 8后不使用方法區(qū),對應堆溢出),查看 class 數(shù)量異常多,可以考慮是否為動態(tài)代理類異常載入過多或類被反復重復加載。
-
方法區(qū)溢出時,查看 class loader 數(shù)量過多,可以考慮是否為自定義 class loader 被異常循環(huán)使用。
-
GC Root 過多,可以查看 GC Root 分布,理論上這種情況極少會遇到,筆者只在 JNI 使用一個存在 BUG 的庫時遇到過。
-
線程數(shù)過多,一般是頻繁創(chuàng)建線程但無法執(zhí)行結束,從概覽可以了解異常表象,具體原因可以參考本文線程分析部分內(nèi)容,此處不展開。
2.2 Dominator tree
注:筆者使用頻率的 Top1,是高效分析 Dump 必看的功能。
功能
-
展現(xiàn)對象的支配關系圖,并給出對象支配內(nèi)存的大小(支配內(nèi)存等同于 Retained Heap,即其被 GC 回收可釋放的內(nèi)存大?。?/p>
-
支持排序、支持按 package、class loader、super class、class 聚類統(tǒng)計
使用入口:全局支配樹: MAT 主界面 → Dominator tree。
舉例: 下圖中通過查看 Dominator tree,了解到內(nèi)存主要是由 ThreadAndListHolder-thread 及 main 兩個線程支配(后面第2.6節(jié)會給出整體案例)。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用場景
-
開始 Dump 分析時,首先應使用 Dominator tree 了解各支配樹起點對象所支配內(nèi)存的大小,進而了解哪幾個起點對象是 GC 無法釋放大內(nèi)存的原因。
-
當個別對象支配樹的 Retained Heap 很大存在明顯傾斜時,可以重點分析占比高的對象支配關系,展開子樹進一步定位到問題根因,如下圖中可看出最終是 SameContentWrapperContainer 對象持有的 ArrayList 過大。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
-
在 Dominator tree 中展開樹狀圖,可以查看支配關系路徑(與 outgoing reference 的區(qū)別是:如果 X 支配 Y,則 X 釋放后 Y必然可釋放;如果僅僅是 X 引用 Y,可能仍有其他對象引用 Y,X 釋放后 Y 仍不能釋放,所以 Dominator tree 去除了 incoming reference 中大量的冗余信息)。
-
有些情況下可能并沒有支配起點對象的 Retained Heap 占用很大內(nèi)存(比如 class X 有100個對象,每個對象的 Retained Heap 是10M,則 class X 所有對象實際支配的內(nèi)存是 1G,但可能 Dominator tree 的前20個都是其他class 的對象),這時可以按 class、package、class loader 做聚合,進而定位目標。
-
下圖中各 GC Roots 所支配的內(nèi)存均不大,這時需要聚合定位爆發(fā)點。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
在 Dominator tree 展現(xiàn)后按 class 聚合,如下圖:
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
可以定位到是 SomeEntry 對象支配內(nèi)存較多,然后結合代碼進一步分析具體原因。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
在一些操作后定位到異常持有 Retained Heap 對象后(如從代碼看對象應該被回收),可以獲取對象的直接支配者,操作方式如下。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
2.3 Histogram 直方圖
注:筆者使用頻率 Top2
功能
-
羅列每個類實例的數(shù)量、類實例累計內(nèi)存占比,包括自身內(nèi)存占用量(Shallow Heap)及支配對象的內(nèi)存占用量(Retain Heap)。
-
支持按對象數(shù)量、Retained Heap、Shallow Heap(默認排序)等指標排序;支持按正則過濾;支持按 package、class loader、super class、class 聚類統(tǒng)計,
使用入口:MAT 主界面 → Histogram;注意 Histogram 默認不展現(xiàn) Retained Heap,可以使用計算器圖標計算,如下圖所示。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用場景
-
有些情況 Dominator tree 無法展現(xiàn)出熱點對象(上文提到 Dominator tree 支配內(nèi)存排名前20的占比均不高,或者按 class 聚合也無明顯熱點對象,此時 Dominator tree 很難做關聯(lián)分析判斷哪類對象占比高),這時可以使用 Histogram 查看所有對象所屬類的分布,快速定位占據(jù) Retained Heap 大頭的類。
使用技巧
-
Integer,String 和 Object[] 一般不直接導致內(nèi)存問題。為更好的組織視圖,可以通過 class loader 或 package 分組進一步聚焦,如下圖。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
Histogram 支持使用正則表達式來過濾。例如,我們可以只展示那些匹配com.q.*的類。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
可以在 Histogram 的某個類繼續(xù)使用 outgoing reference 查看對象分布,進而定位哪些對象是大頭
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
2.4 Leak Suspects
功能:具備自動檢測內(nèi)存泄漏功能,羅列可能存在內(nèi)存泄漏的問題點。
使用入口:一般當存在明顯的內(nèi)存泄漏時,分析完Dump文件后就會展現(xiàn),也可以如下圖在 MAT 主頁 → Leak Suspects。
使用場景:需要查看引用鏈條上占用內(nèi)存較多的可疑對象。這個功能可解決一些基礎問題,但復雜的問題往往幫助有限。
舉例
-
下圖中 Leak Suspects 視圖展現(xiàn)了兩個線程支配了絕大部分內(nèi)存。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
下圖是點擊上圖中 Keywords 中 "Details" ,獲取實例到 GC Root 的最短路徑、dominator 路徑的細信息。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
2.5 Top Consumers
功能:最大對象報告,可以展現(xiàn)哪些類、哪些 class loader、哪些 package 占用最高比例的內(nèi)存,其功能 Histogram 及 Dominator tree 也都支持。
使用場景:應用程序發(fā)生內(nèi)存泄漏時,查看哪些泄漏的對象通常在 Dump 快照中會占很大的比重。因此,對簡單的問題具有較高的價值。
2.6 綜合案例一
使用工具項:Heap dump overview、Dominator tree、Histogram、Class Loader Explorer(見3.4節(jié))、incoming references(見3.1節(jié))
程序代碼
package com.q.mat; import java.util.*; import org.objectweb.asm.*; public class ClassLoaderOOMOps extends ClassLoader implements Opcodes { public static void main(final String args[]) throws Exception { new ThreadAndListHolder(); // ThreadAndListHolder 類中會加載大對象 List<ClassLoader> classLoaders = new ArrayList<ClassLoader>(); final String className = "ClassLoaderOOMExample"; final byte[] code = geneDynamicClassBytes(className); // 循環(huán)創(chuàng)建自定義 class loader,并加載 ClassLoaderOOMExample while (true) { ClassLoaderOOMOps loader = new ClassLoaderOOMOps(); Class<?> exampleClass = loader.defineClass(className, code, 0, code.length); //將二進制流加載到內(nèi)存中 classLoaders.add(loader); // exampleClass.getMethods()[0].invoke(null, new Object[]{null}); // 執(zhí)行自動加載類的方法,通過反射調(diào)用main } } private static byte[] geneDynamicClassBytes(String className) throws Exception { ClassWriter cw = new ClassWriter(0); cw.visit(V1_1, ACC_PUBLIC, className, null, "java/lang/Object", null); //生成默認構造方法 MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); //生成構造方法的字節(jié)碼指令 mw.visitVarInsn(ALOAD, 0); mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); mw.visitInsn(RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); //生成main方法 mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); //生成main方法中的字節(jié)碼指令 mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mw.visitLdcInsn("Hello world!"); mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); mw.visitInsn(RETURN); mw.visitMaxs(2, 2); mw.visitEnd(); //字節(jié)碼生成完成 return cw.toByteArray(); // 獲取生成的class文件對應的二進制流 } }
package com.q.mat; import java.util.*; import org.objectweb.asm.*; public class ThreadAndListHolder extends ClassLoader implements Opcodes { private static Thread innerThread1; private static Thread innerThread2; private static final SameContentWrapperContainerProxy sameContentWrapperContainerProxy = new SameContentWrapperContainerProxy(); static { // 啟用兩個線程作為 GC Roots innerThread1 = new Thread(new Runnable() { public void run() { SameContentWrapperContainerProxy proxy = sameContentWrapperContainerProxy; try { Thread.sleep(60 * 60 * 1000); } catch (Exception e) { System.exit(1); } } }); innerThread1.setName("ThreadAndListHolder-thread-1"); innerThread1.start(); innerThread2 = new Thread(new Runnable() { public void run() { SameContentWrapperContainerProxy proxy = proxy = sameContentWrapperContainerProxy; try { Thread.sleep(60 * 60 * 1000); } catch (Exception e) { System.exit(1); } } }); innerThread2.setName("ThreadAndListHolder-thread-2"); innerThread2.start(); } } class IntArrayListWrapper { private ArrayList<Integer> list; private String name; public IntArrayListWrapper(ArrayList<Integer> list, String name) { this.list = list; this.name = name; } } class SameContentWrapperContainer { // 2個Wrapper內(nèi)部指向同一個 ArrayList,方便學習 Dominator tree IntArrayListWrapper intArrayListWrapper1; IntArrayListWrapper intArrayListWrapper2; public void init() { // 線程直接支配 arrayList,兩個 IntArrayListWrapper 均不支配 arrayList,只能線程運行完回收 ArrayList<Integer> arrayList = generateSeqIntList(10 * 1000 * 1000, 0); intArrayListWrapper1 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-1"); intArrayListWrapper2 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-2"); } private static ArrayList<Integer> generateSeqIntList(int size, int startValue) { ArrayList<Integer> list = new ArrayList<Integer>(size); for (int i = startValue; i < startValue + size; i++) { list.add(i); } return list; } } class SameContentWrapperContainerProxy { SameContentWrapperContainer sameContentWrapperContainer; public SameContentWrapperContainerProxy() { SameContentWrapperContainer container = new SameContentWrapperContainer(); container.init(); sameContentWrapperContainer = container; } }
啟動參數(shù):-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/gjd/Desktop/dump/heapdump.hprof -XX:-UseCompressedClassPointers -XX:-UseCompressedOops
引用關系圖
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
分析過程
-
首先進入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 對象與 main 線程兩者持有99%內(nèi)存不能釋放導致 OOM。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
先來看方向一,在 Heap Dump Overview中可以快速定位到 Number of class loaders 數(shù)達50萬以上,這種基本屬于異常情況,如下圖所示。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用 Class Loader Explorer 分析工具,此時會展現(xiàn)類加載詳情,可以看到有524061個 class loader。我們的案例中僅有ClassLoaderOOMOps 這樣的自定義類加載器,所以很快可以定位到問題。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
如果類加載器較多,不能確定是哪個引發(fā)問題,則可以將所有的 class loader對象按類做聚類,如下圖所示。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
Histogram 會根據(jù) class 聚合,并展現(xiàn)對象數(shù)量及其 Shallow Heap 及 Retained Heap(如Retained Heap項目為空,可以點擊下圖中計算機的圖標并計算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044個對象,其 Retain Heap 占據(jù)了370M以上(上述代碼是100M左右)。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用 incoming references,可以找到創(chuàng)建的代碼位置。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
再來看方向二,同樣在占據(jù)319M內(nèi)存的 Obejct 數(shù)組采用 incoming references 查看引用路徑,也很容易定位到具體代碼位置。并且從下圖中我們看出,Dominator tree 的起點并不一定是 GC根,且通過 Dominator tree 可能無法獲取到最開始的創(chuàng)建路徑,但 incoming references 是可以的。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
3. 對象間依賴詳解及實戰(zhàn)
3.1 References
注:筆者使用頻率 Top2
功能:在對象引用圖中查看某個特定對象的所有引用關系(提供對象對其他對象或基本類型的引用關系,以及被外部其他對象的引用關系)。通過任一對象的直接引用及間接引用詳情(主要是屬性值及內(nèi)存占用),提供完善的依賴鏈路詳情。
使用入口:目標域右鍵 → List objects → with outgoing references/with incoming references.
使用場景
-
outgoing reference:查看對象所引用的對象,并支持鏈式傳遞操作。如查看一個大對象持有哪些內(nèi)容,當一個復雜對象的 Retained Heap 較大時,通過 outgoing reference 可以查看由哪個屬性引發(fā)的。下圖中 A 支配 F,且 F 占據(jù)大量內(nèi)存,但優(yōu)化時 F 的直接支配對象 A 無法修改。可通過 outgoing reference 看關系鏈上 D、B、E、C,并結合業(yè)務邏輯優(yōu)化中間環(huán)節(jié),這依托 dominator tree 是做不到的。
-
incoming reference:查看對象被哪些對象引用,并支持鏈式傳遞操作。如查看一個大對象都被哪些對象引用,下圖中 K 占內(nèi)存大,所以 J 的 Retained Heap 較大,目標是從 GC Roots 摘除 J 引用,但在 Dominator tree 上 J 是樹根,無法獲取其被引用路徑,可通過 incoming reference 查看關系鏈上的 H、X、Y ,并結合業(yè)務邏輯將 J 從 GC Root 鏈摘除。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
3.2 Thread overview
功能:展現(xiàn)轉(zhuǎn)儲 dump 文件是線程執(zhí)行棧、線程棧引用的對象等詳細狀態(tài),也提供各線程的 Retained Heap 等關聯(lián)內(nèi)存信息。
使用入口:MAT 主頁 → Thread overview
使用場景
-
查看不同線程持有的內(nèi)存占比,定位高內(nèi)存消耗線程(開發(fā)技巧:不要直接使用 Thread 或 Executor 默認線程名避免全部混合在一起,使用線程盡量自命名方便識別,如下圖中 ThreadAndListHolder-thread 是自定義線程名,可以很容易定位到具體代碼)
-
查看線程的執(zhí)行棧及變量,結合業(yè)務代碼了解線程阻塞在什么地方,以及無法繼續(xù)運行釋放內(nèi)存,如下圖中 ThreadAndListHolder-thread 阻塞在 sleep 方法。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
3.3 Path To GC Roots
功能:提供任一對象到 GC Root 的路徑詳情。
使用入口:目標域右鍵 → Path To GC Roots
使用場景:有時你確信已經(jīng)處理了大的對象集合但依然無法回收,該功能能快速定位異常對象不能被 GC 回收的原因,直擊異常對象到 GC Root 的引用路徑。比 incoming reference 的優(yōu)勢是屏蔽掉很多不需關注的引用關系,比 Dominator tree 的優(yōu)勢是可以得到更全面的信息。
小技巧:在排查內(nèi)存泄漏時,建議選擇 exclude all phantom/weak/soft etc.references 排除虛引用/弱引用/軟引用等的引用鏈,因為被虛引用/弱引用/軟引用的對象可以直接被 GC 給回收,聚焦在對象是否還存在 Strong 引用鏈即可。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
3.4 class loader 分析
功能
-
查看堆中所有 class loader 的使用情況(入口:MAT 主頁菜單藍色桶圖標 → Java Basics → Class Loader Explorer)。
-
查看堆中被不同class loader 重復加載的類(入口:MAT 主頁菜單藍色桶圖標 → Java Basics → Duplicated Classes)。
使用場景
-
當從 Heap dump overview 了解到系統(tǒng)中 class loader 過多,導致占用內(nèi)存異常時進入更細致的分析定位根因時使用。
-
解決 NoClassDefFoundError 問題或檢測 jar 包是否被重復加載
具體使用方法在 2.6 及 3.5 兩節(jié)的案例中有介紹。
3.5 綜合案例二
使用工具項:class loader(重復類檢測)、inspector、正則檢索。
異?,F(xiàn)象 :運行時報 NoClassDefFoundError,在 classpath 中有兩個不同版本的同名類。
分析過程
-
進入 MAT 已加載的重復類檢測功能,方式如下圖。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
可以看到所有重復的類,以及相關的類加載器,如下圖。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
-
根據(jù)類名,在<Regex>框中輸入類名可以過濾無效信息。
-
選中目標類,通過Inspector視圖,可以看到被加載的類具體是在哪個jar包里。(本例中重復的類是被 URLClassloader 加載的,右鍵點擊 “_context” 屬性,最后點擊 “Go Into”,在彈出的窗口中的屬性 “_war” 值是被加載類的具體包位置)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
4. 對象狀態(tài)詳解及實戰(zhàn)
4.1 inspector
功能:MAT 通過 inspector 面板展現(xiàn)對象的詳情信息,如靜態(tài)屬性值及實例屬性值、內(nèi)存地址、類繼承關系、package、class loader、GC Roots 等詳情數(shù)據(jù)。
使用場景
-
當內(nèi)存使用量與業(yè)務邏輯有較強關聯(lián)的場景,通過 inspector 可以通過查看對象具體屬性值。比如:社交場景中某個用戶對象的好友列表異常,其 List 長度達到幾億,通過 inspector 面板獲取到異常用戶 ID,進而從業(yè)務視角繼續(xù)排查屬于哪個用戶,本里可能有系統(tǒng)賬號,與所有用戶是好友。
-
集合等類型的使用會較多,如查看 ArrayList 的 size 屬性也就了解其大小。
舉例:下圖中左邊的 Inspector 窗口展現(xiàn)了地址 0x125754cf8 的 ArrayList 實例詳情,包括 modCount 等并不會在 outgoing references 展現(xiàn)的基本屬性。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
4.2 集合狀態(tài)
功能:幫助更直觀的了解系統(tǒng)的內(nèi)存使用情況,查找浪費的內(nèi)存空間。
使用入口:MAT 主頁 → Java Collections → 填充率/Hash沖突等功能。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用場景
-
通過對 ArrayList 或數(shù)組等集合類對象按填充率聚類,定位稀疏或空集合類對象造成的內(nèi)存浪費。
-
通過 HashMap 沖突率判定 hash 策略是否合理。
具體使用方法在 4.3 節(jié)案例詳細介紹。
4.3 綜合案例三
使用工具項:Dominator tree、Histogram、集合 ratio。
異?,F(xiàn)象 :程序 OOM,且 Dominator tree 無大對象,通過 Histogram 了解到多個 ArrayList 占據(jù)大量內(nèi)存,期望通過減少 ArrayList 優(yōu)化程序。
程序代碼
package com.q.mat; import java.util.ArrayList; import java.util.List; public class ListRatioDemo { public static void main(String[] args) { for(int i=0;i<10000;i++){ Thread thread = new Thread(new Runnable() { public void run() { HolderContainer holderContainer1 = new HolderContainer(); try { Thread.sleep(1000 * 1000 * 60); } catch (Exception e) { System.exit(1); } } }); thread.setName("inner-thread-" + i); thread.start(); } } } class HolderContainer { ListHolder listHolder1 = new ListHolder().init(); ListHolder listHolder2 = new ListHolder().init(); } class ListHolder { static final int LIST_SIZE = 100 * 1000; List<String> list1 = new ArrayList(LIST_SIZE); // 5%填充 List<String> list2 = new ArrayList(LIST_SIZE); // 5%填充 List<String> list3 = new ArrayList(LIST_SIZE); // 15%填充 List<String> list4 = new ArrayList(LIST_SIZE); // 30%填充 public ListHolder init() { for (int i = 0; i < LIST_SIZE; i++) { if (i < 0.05 * LIST_SIZE) { list1.add("" + i); list2.add("" + i); } if (i < 0.15 * LIST_SIZE) { list3.add("" + i); } if (i < 0.3 * LIST_SIZE) { list4.add("" + i); } } return this; } }
分析過程
-
使用 Dominator tree 查看并無高占比起點。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
使用 Histogram 定位到 ListHolder 及 ArrayList 占比過高,經(jīng)過業(yè)務分析很多 List 填充率很低,不會浪費內(nèi)存。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
查看 ArrayList 的填充率,MAT 首頁 → Java Collections → Collection Fill Ratio。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
查看類型填寫 java.util.ArrayList。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
從結果可以看出絕大部分 ArrayList 初始申請長度過大。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
5. 按條件檢索詳解及實戰(zhàn)
5.1 OQL
功能:提供一種類似于SQL的對象(類)級別統(tǒng)一結構化查詢語言,根據(jù)條件對堆中對象進行篩選。
語法
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
-
Select 子句可以使用“*”,查看結果對象的引用實例(相當于 outgoing references);可以指定具體的內(nèi)容,如 Select OBJECTS v.elementData from xx 是返回的結果是完整的對象,而不是簡單的對象描述信息);可以使用 Distinct 關鍵詞去重。
-
From 指定查詢范圍,一般指定類名、正則表達式、對象地址。
-
Where 用來指定篩選條件。
-
全部語法詳見:OQL 語法
-
未支持的核心功能:group by value,如果有需求可以先導出結果到 csv 中,再使用 awk 等腳本工具分析即可。
例子:查找 size=0 且未使用過的 ArrayList:select * from java.util.ArrayList where size=0 and modCount=0。
使用場景
-
一般比較復雜的問題會使用 OQL,而且這類問題往往與業(yè)務邏輯有較大關系。比如大量的小對象整體占用內(nèi)存高,但預期小對象應該不會過多(比如達到百萬個),一個一個看又不現(xiàn)實,可以采用 OQL 查詢導出數(shù)據(jù)排查。
例如:微服務的分布式鏈路追蹤系統(tǒng),采集各服務所有接口名,共計200個服務卻采集到了200萬個接口名(一個服務不會有1萬個接口),這時直接在 List 中一個個查看很難定位,可以直接用 OQL 導出,定位哪個服務接口名收集異常(如把 URL 中 ID 也統(tǒng)計到接口中了)
5.2 檢索及篩選
功能:本文第二章內(nèi)存分布,第三章對象間依賴的眾多功能,均支持按字符串檢索、按正則檢索等操作。
使用場景:在使用 Histogram、Thread overview 等功能時,可以進一步添加字符串匹配、正則匹配條件過濾縮小排查范圍。
5.3 按地址尋址
功能:根據(jù)對象的虛擬內(nèi)存十六進制地址查找對象。
使用場景:僅知道地址并希望快速查看對象做后續(xù)分析時使用,其余可以直接使用 outgoing reference 了解對象信息。
5.4 綜合案例四
使用工具項:OQL、Histogram、incoming references
異常現(xiàn)象及目的 :程序占用內(nèi)存高,存在默認初始化較長的 ArrayList,需分析 ArrayList 被使用的占比,通過數(shù)據(jù)支撐是否采用懶加載模式,并分析具體哪塊代碼創(chuàng)建了空 ArrayList。
程序代碼
public class EmptyListDemo { public static void main(String[] args) { EmptyValueContainerList emptyValueContainerList = new EmptyValueContainerList(); FilledValueContainerList filledValueContainerList = new FilledValueContainerList(); System.out.println("start sleep..."); try { Thread.sleep(50 * 1000 * 1000); } catch (Exception e) { System.exit(1); } } } class EmptyValueContainer { List<Integer> value1 = new ArrayList(10); List<Integer> value2 = new ArrayList(10); List<Integer> value3 = new ArrayList(10); } class EmptyValueContainerList { List<EmptyValueContainer> list = new ArrayList(500 * 1000); public EmptyValueContainerList() { for (int i = 0; i < 500 * 1000; i++) { list.add(new EmptyValueContainer()); } } } class FilledValueContainer { List<Integer> value1 = new ArrayList(10); List<Integer> value2 = new ArrayList(10); List<Integer> value3 = new ArrayList(10); public FilledValueContainer init() { value1.addAll(Arrays.asList(1, 3, 5, 7, 9)); value2.addAll(Arrays.asList(2, 4, 6, 8, 10)); value1.addAll(Arrays.asList(1, 1, 1, 1, 1, 1, 1, 1, 1, 1)); return this; } } class FilledValueContainerList { List<FilledValueContainer> list = new ArrayList(500); public FilledValueContainerList() { for (int i = 0; i < 500; i++) { list.add(new FilledValueContainer().init()); } } }
分析過程
-
內(nèi)存中有50萬個 capacity = 10 的空 ArrayList 實例。我們分析下這些對象的占用內(nèi)存總大小及對象創(chuàng)建位置,以便分析延遲初始化(即直到使用這些對象的時候才將之實例化,否則一直為null)是否有必要。
-
使用 OQL 查詢出初始化后未被使用的 ArrayList(size=0 且 modCount=0),語句如下圖??梢钥闯龉?150 萬個空 ArrayList,這些對象屬于浪費內(nèi)存。我們接下來計算下總計占用多少內(nèi)存,并根據(jù)結果看是否需要優(yōu)化。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
計算 150萬 ArrayList占內(nèi)存總量,直接點擊右上方帶黃色箭頭的 Histogram 圖標,這個圖標是在選定的結果再用直方圖展示,總計支配了120M 左右內(nèi)存(所以這里點擊結果,不包含 modCount 或 size 大于0的 ArrayList 對象)。這類在選定結果繼續(xù)分析很多功能都支持,如正則檢索、Histogram、Dominator tree等等。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
查看下圖 ArrayList 的具體來源,可用 incoming references,下圖中顯示了清晰的對象創(chuàng)建路徑。
?
編輯切換為居中
添加圖片注釋,不超過 140 字(可選)
總結
至此本文講解了 MAT 各項工具的功能、使用方法、適用場景,也穿插了4個實戰(zhàn)案例,熟練掌握對分析 JVM 內(nèi)存問題大有裨益,尤其是各種功能的組合使用。文章來源:http://www.zghlxwxcb.cn/news/detail-699961.html
??????資源獲取:
大家 點贊、收藏、關注、評論啦 、 查看???????????? 微信公眾號獲取聯(lián)系方式????????????
精彩專欄推薦訂閱:在 下方專欄????????????????
每天學四小時:Java+Spring+JVM+分布式高并發(fā),架構師指日可待
文章來源地址http://www.zghlxwxcb.cn/news/detail-699961.html
到了這里,關于一文深度講解JVM 內(nèi)存分析工具 MAT及實踐(建議收藏)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!