一、MAT下載和安裝
1、概述

MAT(Memory Analyzer Tool)工具是一款功能強大的]ava堆內存分析器。可以用於查找內存泄漏以及查看內存消耗情況。MAT是基於Eclipse開發的,不僅可以單獨使用,還可以作為插件的形式嵌入在Eclipse中使用。是一款免費的性能分析工具,使用起來非常方便。
2、下載地址:

https://www.eclipse.org/mat/downloads.php

我目前電腦的JDK安裝環境是1.8的,所以需要下載對應JDK1.8版本的MAT版本


 


 

3、安裝

下載後解壓,點擊MemoryAnalyzer.exe進行啓動


 

4、安裝出現的報錯問題

4.1、MAT版本和JDK版本不一致

問題描述:
要是直接下載最新版的MAT,可能需要高版本JDK才行。啓動是需要JDK11或者更高的版本,我本地JDK版本是1.8,所以會報JDK版本不適合。


 解決方法:
在MemoryAnalyzer.ini 中加入指定jdk的地址, (jdk不用安裝直接下載 解壓指定bin/javaw.exe就可)

-vm
D:/java/jdk1.8.0_211/binbin/javaw.exe
-vm
D:/java/jdk1.8.0_211/binbin/javaw.exe
-startup
plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.700.v20180518-1200
-vmargs
-Xmx1024m

4.2、堆dump文件較大、使用MAT打開的時候總是拋出 Java Heap Error

問題描述:
有時候線上產生的堆dump文件較大,如果你的hprof文件沒有問題的話,使用MAT打開的時候總是拋出 Java Heap Error. 可能是默認的1024m內存不夠用了。

解決辦法:
找到MAT的安裝目錄,找到MemoryAnalyzer.ini 修改其中的-Xmx即可


 將-Xmx1024m 調大即可


 

5、jmap命令拿到dump日誌文件

jmap是Java虛擬機自帶的一個命令行工具,可以用來生成JVM內存快照(Heap dump)文件。以下是使用jmap命令生成dump文件的步驟:

jmap -dump:format=b,file=heap.bin <pid>

通常情況下,在生產環境中使用jmap命令生成Heap dump文件時,建議把生成的文件下載到本地進行分析,以減少對生產環境的干擾。另外,在生成Heap dump文件時,一定要確保Java應用程序正常運行,否則可能會導致生成的文件不完整或者無法正確解析。

二、MAT工具排查分析OOM
1、故障現象:

集羣應用服務器在高併發請求的情況下會不定時地因為響應超時而報警,但是很快又超時解除,恢復正常,如此反覆,讓運維人員非常苦惱。
原因分析: 來到一家新公司,一個重構項目的開發人員估計搞不動了,最後選擇跑路,我的到來正好接盤了這個有好多bug的項目。配合測試功能測試完結束後,進行壓測。發現查詢接口只要併發一起來就會出現錯亂的現象。先是排查原先寫的代碼是否有問題,沒發現問題,然後我以為是髒讀,調整的事務的隔離級別等等方法,發現還是解決不了。最後沒辦法在方法上加了一個synchronized鎖,再進行壓測時,雖然吞吐量不高,但是不會有報錯的現象。等開始正式切換系統進行上線時。因為每天會有至少20w的查詢量。只要某個時間段只要請求量很高就會出現連接超時的現象。當時也想到是因為加了synchronized造成高併發請求下,很多請求一直在等待,最後因為時間太久而造成的超時。所以我就下載了dump文件,使用MAT工具進行分析。果然是這個鎖造成的。這是的我已經對代碼稍微熟悉了,分析什麼造成的錯亂現象。發現代碼有一處用到了共享變量,造成每次高併發去請求出現的錯亂現象。我當時心裏。。。


 參考 文檔,確實是synchronized造成的


 下面詳説一下MAT如何分析dump文件
2、MAT 分析 OOM 問題通常思路:

    通過支配樹功能或直方圖功能查看消耗內存最大的類型,來分析內存泄露的大概原因;
    查看那些消耗內存最大的類型、詳細的對象明細列表,以及它們的引用鏈,來定位內存泄露的具體點;
    配合查看對象屬性的功能,可以脱離源碼看到對象的各種屬性的值和依賴關係,幫助我們理清程序邏輯和參數;
    輔助使用查看線程棧來看 OOM 問題是否和過多線程有關,甚至可以在線程棧看到 OOM 最後一刻出現異常的線程。

3、使用MAT定位問題:

    定位問題方式一:

    現在有一個OOM後得到的堆轉儲文件 java_pid29569.hprof,現在要使用 MAT 的直方圖、支配樹、線程棧、OQL 等功能來分析此次 OOM 的原因。

    首先,用 MAT 打開後先進入的是概覽信息界面,可以看到整個堆是 437.6MB:


 那麼,這 437.6MB 都是什麼對象呢?

如圖所示,工具欄的第二個按鈕可以打開直方圖,直方圖按照類型進行分組,列出了每個類有多少個實例,以及佔用的內存。可以看到,char[]字節數組佔用內存最多,對象數量也很多,結合第二位的 java.lang.String 類型對象數量也很多,大概可以猜出(String 使用 char[]作為實際數據存儲)程序可能是被字符串佔滿了內存,導致 OOM。


 在 char[]上點擊右鍵,選擇 List objects->with incoming references,就可以列出所有的 char[]實例,以及每個 char[]的整個引用關係鏈:


 接下來,我們按照紅色框中的引用鏈來查看,嘗試找到這些大 char[]的來源:

在①處看到,這些 char[]幾乎都是 10000 個字符、佔用 20000 字節左右(char 是 UTF-16,每一個字符佔用 2 字節);

在②處看到,char[]被 String 的 value 字段引用,説明 char[]來自字符串;

在③處看到,String 被 ArrayList 的 elementData 字段引用,説明這些字符串加入了一個 ArrayList 中;

在④處看到,ArrayList 又被 FooService 的 data 字段引用,這個 ArrayList 整個 RetainedHeap 列的值是 431MB。

左側的藍色框可以查看每一個實例的內部屬性,圖中顯示 FooService 有一個 data 屬性,類型是 ArrayList。

Retained Heap(深堆):代表對象本身和對象關聯的對象佔用的內存;

Shallow Heap(淺堆):代表對象本身佔用的內存。
比如,我們的 FooService 中的 data 這個 ArrayList 對象本身只有 16 字節,但是其所有關聯的對象佔用了 431MB 內存。這些就可以説明,肯定有哪裏在不斷向這個 List 中添加 String 數據,導致了 OOM。

如果我們希望看到字符串完整內容的話,可以右鍵選擇 Copy->Value,把值複製到剪貼板或保存到文件中:


 這裏,我們複製出的是 10000 個字符 a(下圖紅色部分可以看到)。
看到這些,我們已經基本可以還原出真實的代碼是怎樣的了,定位到了問題代碼。

定位問題方式二:
其實,我們之前使用直方圖定位 FooService,已經走了些彎路。你可以點擊工具欄中第三個按鈕(下圖左上角的紅框所示)進入支配樹界面。這個界面會按照對象的 Retained Heap 倒序直接列出佔用內存最大的對象。


 可以看到,第一位就是 FooService,整個路徑是 FooSerice->ArrayList->Object[]->String->char[](藍色框部分),一共有 21523 個字符串(綠色方框部分)。通常使用這種方式可以一步到位的定位出問題所在。

藉助MAT尋到具體問題原因
我們就從內存角度定位到 FooService 是根源了。那麼,OOM 的時候,FooService 是在執行什麼邏輯呢?

為解決這個問題,我們可以點擊工具欄的第五個按鈕(下圖紅色框所示)。打開線程視圖,首先看到的就是一個名為 main 的線程(Name 列),展開後果然發現了 FooService:


 先執行的方法先入棧,所以線程棧最上面是線程當前執行的方法,逐一往下看能看到整個調用路徑。

因為我們希望瞭解 FooService.oom() 方法,看看是誰在調用它,它的內部又調用了誰,所以選擇以 FooService.oom() 方法(藍色框)為起點來分析這個調用棧。

往下看整個綠色框部分,oom() 方法被 OOMApplication 的 run 方法調用,而這個 run 方法又被 SpringAppliction.callRunner 方法調用。

看到參數中的 CommandLineRunner 你應該能想到,OOMApplication 其實是實現了 CommandLineRunner 接口,所以是 SpringBoot 應用程序啓動後執行的。

以 FooService 為起點往上看,從紫色框中的 Collectors 和 IntPipeline,大概也可以猜出,這些字符串是由 Stream 操作產生的。

再往上看,可以發現在 StringBuilder 的 append 操作的時候,出現了 OutOfMemoryError 異常(黑色框部分),説明這這個線程拋出了 OOM 異常。

我們看到,整個程序是 Spring Boot 應用程序,那麼 FooService 是不是 Spring 的 Bean 呢,又是不是單例呢?

如果能分析出這點的話,就更能確認是因為反覆調用同一個 FooService 的 oom 方法,然後導致其內部的 ArrayList 不斷膨脹。

點擊工具欄的第四個按鈕(如下圖紅框所示),來到 OQL 界面。在這個界面,我們可以使用類似 SQL 的語法,在 dump 中搜索數據(你可以直接在 MAT 幫助菜單搜索 OQL Syntax,來查看 OQL 的詳細語法)。


 比如,輸入如下語句搜索 FooService 的實例:

SELECT * FROM org.geekbang.time.commonmistakes.troubleshootingtools.oom.FooService


 可以看到只有一個實例,然後我們通過 List objects 功能搜索引用 FooService 的對象:


     可以看到,一共兩處引用:

    第一處是,OOMApplication 使用了 FooService。

    第二處是一個 ConcurrentHashMap。可以看到,這個 HashMap 是 DefaultListableBeanFactory 的 singletonObjects 字段,可以證實 FooService 是 Spring 容器管理的單例的 Bean。

4、結論

到現在為止,雖然沒看程序代碼,但是已經大概知道程序出現 OOM 的原因和大概的調用棧了。再貼出程序來對比一下,果然和我們看到得一模一樣:

@SpringBootApplication
public class OOMApplication implements CommandLineRunner {
    @Autowired
    FooService fooService;
    public static void main(String[] args) {
        SpringApplication.run(OOMApplication.class, args);
    }
    @Override
    public void run(String... args) throws Exception {
        //程序啓動後,不斷調用Fooservice.oom()方法
        while (true) {
            fooService.oom();
        }
    }
}
@Component
public class FooService {
    List<String> data = new ArrayList<>();
    public void oom() {
        //往同一個ArrayList中不斷加入大小為10KB的字符串
        data.add(IntStream.rangeClosed(1, 10_000)
                .mapToObj(__ -> "a")
                .collect(Collectors.joining("")));
    }
}

到這裏,我們使用 MAT 工具從對象清單、大對象、線程棧等視角,分析了一個 OOM 程序的堆轉儲。可以發現,有了堆轉儲,幾乎相當於拿到了應用程序的源碼 + 當時那一刻的快照,OOM 的問題無從遁形。
三、jvm-jps、jinfo、jstat、jstack、jmap 基本使用

給系統定位問題的時候,知識經驗是基礎,應用數據是依據,工具是手段,在jvm中,我們常見的數據包括: 運行日誌、堆棧信息、GC信息、線程快照(threaddump/javacode)、堆快照(heapdump/hporf),jdk提供給我們了很實用的工具來分析,定位解決這些問題,這些工具包含於jdk中,並且以java實現,方便在不同的環境中不用安裝其他依賴庫即可使用,很是方便。下面分別介紹 jps、jinfo、jstat、jstack、jmap,本文使用的jdk版本為1.8.0_11
1、jps ( jvm process status tool ) 虛擬機進程工具

-q    忽略主類的名稱,只輸出pid
-m    輸出啓動類main函數的參數
-l    輸出主類名,如果進程執行的為jar,則輸出jar路徑
-v    輸出具體進程啓動時jvm參數

1.命名格式

jps [options] pid

2.常用方式

    jps -lv : 輸出啓動類名與啓動時jvm參數,可以方便的看到各個tomcat的自定義參數配置
    jps -lv |grep project_name : 在上述基礎上過濾出自己想要查看的項目的信息

2、jinfo ( configuration info for java ) 顯示虛擬機配置信息

1.常用用法

    jinfo pid : 顯示jvm系統屬性與vm參數信息
    jinfo -flags pid : 顯示jvm vm參數信息,如最大最小堆,默認堆,垃圾收集器參數等
    jinfo -sysprops pid : 顯示jvm系統屬性
    jinfo -flag : 顯示特定vm參數值,例如 jinfo -flag MaxHeapSize pid 輸出pid的最大堆內存

3、jstat ( jvm statistics monitoring tool) 收集虛擬機各方面運行數據

1、語法格式

jstat [ option pid [interval[s|ms] [count]]]

説明: interval 表示循環時間間隔,默認單位為ms,可以在直接使用s/ms指定單位,如 60ms/1s, count 表示輸出幾次 例:

jstat gc pid 1s 20

每1s查詢一次gc情況,查詢20次

2、option 詳解(主要分三類:類裝載、垃圾收集、運行期編譯狀況)

-class    監視類裝載、卸載數量、總空間以及類裝載所耗費的時間
-gc    監視Java堆,包括Eden區、兩survivor區、老年代、永久代等的容量、已用空間、GC時間合計等
-gccapacity    與-gc基本相同,但關注點為Java堆各個區域使用到的最大、最小空間
-gcutil    與-gc基本相同,但關注點為Java堆各個區域已使用空間佔總空間的百分比
-gccause    與-gcutil功能相同,但會額外輸出導致上一次GC產生的原因
-gcnew    監控新生代GC情況
-gcnewcapacity    與-gcnew基本相同,但關注最大,最小空間
-gold    監控老年代GC情況
-goldcapacity    與-gcold基本相同,但關注最大,最小空間
-compiler    輸出被JIT編譯過的方法、耗時等信息
-printcomplilation    輸出已經被JIT編譯的方法

3、查看類裝載卸載情況 jstat -class pid

Loaded    裝載總數量
Bytes    裝載總大小
Unloaded    卸載類的數量
Time    加載和卸載類總共的耗時

4、查看GC情況 jstat -gc pid

S0C    新生代survivor0容量
S1C    新生代survivor1容量
S0U    新生代survivor0已使用大小
S1U    新生代survivor1已使用大小
EC    新生代eden區容量
EU    新生代eden區已使用大小
OC    老年代容量
OU    老年代已使用大小
MC    元數據容量,即方法區容量
MU    元數據已使用空間
CCSC    壓縮類空間大小
CCSU    壓縮類空間使用大小
YGC    新生代gc次數(young gc)
YGCT    新生代gc時間(s)
FGC    老生代gc次數(full gc)
GCT    總的gc時間,包括young gc和full gc

5、查看GC情況,以百分比顯示

jstat -gcutil pid

6、查看新生代GC情況

jstat -gcnew pid
S0C    新生代survivor0容量
S1C    新生代survivor1容量
S0U    新生代survivor0已使用大小
S1U    新生代survivor1已使用大小
TT    對象在新生代存活的次數
MTT    對象在新生代存活的最大次數
DSS    期望的倖存區大小
EC    eden區大小
EU    eden區已使用大小
YGC    young gc次數
YGCT    young gc 時間 (秒)

7、查看老年代GC情況

jstat -gcold pid

8、查看各空間容量

jstat -gccapacity pid
NGCMN    新生代最小容量
NGCMX    新生代最大容量
NGC    當前新生代容量
S0C    survivor0大小
S1C    survivor0大小
EC    EDEN區大小
OGCMN    老年代最小容量
OGCMX    老年代最大容量
OGC    當前老年代大小
MCMN    元數據最小容量
MCMX    元數據最大容量
CCSMN    最小壓縮類空間大小
CCSMX    最大壓縮類空間大小
CCSC    當前壓縮類空間大小)

9、查看編譯情況

jstat -compiler pid

4、jmap ( memory map for java ) 虛擬機堆快照工具

1、常用用法

jmap -heap pid 查看當前jvm heapdump與垃圾收集器的使用情況
jmap -dump:format=b,file=/temp/filename.hprof pid 轉儲堆快照,生成hprof文件到指定路徑
jmap -histo pid 列出當前heap中對象狀況,附字節碼與java對象映射表

5、jstack ( stack trace for java ) 虛擬機線程快照工具

效果演示: 會顯示所有線程的各種信息,可以用來排查死鎖,或線程長時間停滯的問題…