我們知道,在存儲用戶輸入的密碼時,會使用一些 hash 算法對密碼進行加工,比如 SHA-1。這些信息同樣不允許在日志輸出里出現,必須做脫敏處理,但是對于一個擁有系統權限的攻擊者來說,這些防護依然是不夠的。攻擊者可能會直接從內存中獲取明文數據,尤其是對于 Java 來說,由于提供了 jmap 這一類非常方便的工具,可以把整個堆內存的數據 dump 下來。
比如,“我的世界”這一類使用 Java 開發的游戲,會比其他語言的游戲更加容易破解一些,所以我們在 JVM 中,如果把密碼存儲為 char 數組,其安全性會稍微高一些。
這是一把雙刃劍,在保證安全的前提下,我們也可以借助一些外部的分析工具,幫助我們方便的找到問題根本。
有兩種方式來獲取內存的快照。我們前面提到過,通過配置一些參數,可以在發生 OOM 的時候,被動 dump 一份堆棧信息,這是一種;另一種,就是通過 jmap 主動去獲取內存的快照。
jmap 命令在 Java 9 之后,使用 jhsdb 命令替代,它們在用法上,區別不大。注意,這些命令本身會占用操作系統的資源,在某些情況下會造成服務響應緩慢,所以不要頻繁執行。
```
jmap?-dump:format=b,file=heap.bin?37340
jhsdb?jmap??--binaryheap?--pid??37340
```
#### 1. 工具介紹
有很多工具能夠幫助我們來分析這份內存快照。在前面已多次提到 VisualVm 這個工具,它同樣可以加載和分析這份 dump 數據,雖然比較“寒磣”。
專業的事情要有專業的工具來做,今天要介紹的是一款專業的開源分析工具,即 MAT。
MAT 工具是基于 Eclipse 平臺開發的,本身是一個 Java 程序,所以如果你的堆快照比較大的話,則需要一臺內存比較大的分析機器,并給 MAT 本身加大初始內存,這個可以修改安裝目錄中的 MemoryAnalyzer.ini 文件。
來看一下 MAT 工具的截圖,主要的功能都體現在工具欄上了。其中,默認的啟動界面,展示了占用內存最高的一些對象,并有一些常用的快捷方式。通常,發生內存泄漏的對象,會在快照中占用比較大的比重,分析這些比較大的對象,是我們切入問題的第一步。

點擊對象,可以瀏覽對象的引用關系,這是一個非常有用的功能:
* outgoing references 對象的引出
* incoming references ?對象的引入
**path to GC Roots** 這是快速分析的一個常用功能,顯示和 GC Roots 之間的路徑。

另外一個比較重要的概念,就是淺堆(Shallow Heap)和深堆(Retained Heap),在 MAT 上經常看到這兩個數值。

淺堆代表了對象本身的內存占用,包括對象自身的內存占用,以及“為了引用”其他對象所占用的內存。
深堆是一個統計結果,會循環計算引用的具體對象所占用的內存。但是深堆和“對象大小”有一點不同,深堆指的是一個對象被垃圾回收后,能夠釋放的內存大小,這些被釋放的對象集合,叫做保留集(Retained Set)。

如上圖所示,A 對象淺堆大小 1 KB,B 對象 2 KB,C 對象 100 KB。A 對象同時引用了 B 對象和 C 對象,但由于 C 對象也被 D 引用,所以 A 對象的深堆大小為 3 KB(1 KB + 2 KB)。
A 對象大小(1 KB + 2 KB + 100 KB)> A 對象深堆 > A 對象淺堆。
#### 2. 代碼示例
```
import?java.util.ArrayList;
import?java.util.HashMap;
import?java.util.List;
import?java.util.Map;
import?java.util.stream.IntStream;
public?class?Objects4MAT?{
????static?class?A4MAT?{
????????B4MAT?b4MAT?=?new?B4MAT();
????}
????static?class?B4MAT?{
????????C4MAT?c4MAT?=?new?C4MAT();
????}
????static?class?C4MAT?{
????????List<String>?list?=?new?ArrayList<>();
????}
????static?class?DominatorTreeDemo1?{
????????DominatorTreeDemo2?dominatorTreeDemo2;
????????public?void?setValue(DominatorTreeDemo2?value)?{
????????????this.dominatorTreeDemo2?=?value;
????????}
????}
????static?class?DominatorTreeDemo2?{
????????DominatorTreeDemo1?dominatorTreeDemo1;
????????public?void?setValue(DominatorTreeDemo1?value)?{
????????????this.dominatorTreeDemo1?=?value;
????????}
????}
????static?class?Holder?{
????????DominatorTreeDemo1?demo1?=?new?DominatorTreeDemo1();
????????DominatorTreeDemo2?demo2?=?new?DominatorTreeDemo2();
????????Holder()?{
????????????demo1.setValue(demo2);
????????????demo2.setValue(demo1);
????????}
????????private?boolean?aBoolean?=?false;
????????private?char?aChar?=?'\0';
????????private?short?aShort?=?1;
????????private?int?anInt?=?1;
????????private?long?aLong?=?1L;
????????private?float?aFloat?=?1.0F;
????????private?double?aDouble?=?1.0D;
????????private?Double?aDouble_2?=?1.0D;
????????private?int[]?ints?=?new?int[2];
????????private?String?string?=?"1234";
????}
????Runnable?runnable?=?()?->?{
????????Map<String,?A4MAT>?map?=?new?HashMap<>();
????????IntStream.range(0,?100).forEach(i?->?{
????????????byte[]?bytes?=?new?byte[1024?*?1024];
????????????String?str?=?new?String(bytes).replace('\0',?(char)?i);
????????????A4MAT?a4MAT?=?new?A4MAT();
????????????a4MAT.b4MAT.c4MAT.list.add(str);
????????????map.put(i?+?"",?a4MAT);
????????});
????????Holder?holder?=?new?Holder();
????????try?{
????????????//sleep?forever?,?retain?the?memory
????????????Thread.sleep(Integer.MAX_VALUE);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????};
????void?startHugeThread()?throws?Exception?{
????????new?Thread(runnable,?"huge-thread").start();
????}
????public?static?void?main(String[]?args)?throws?Exception?{
????????Objects4MAT?objects4MAT?=?new?Objects4MAT();
????????objects4MAT.startHugeThread();
????}
}
```
* 2.1. 代碼介紹
我們以一段代碼示例 Objects4MAT,來具體看一下 MAT 工具的使用。代碼創建了一個新的線程 "huge-thread",并建立了一個引用的層級關系,總的內存大約占用 100 MB。同時,demo1 和 demo2 展示了一個循環引用的關系。最后,使用 sleep 函數,讓線程永久阻塞住,此時整個堆處于一個相對“靜止”的狀態。

如果你是在本地啟動的示例代碼,則可以使用 Accquire 的方式來獲取堆快照。

* 2.2. 內存泄漏檢測
如果問題特別突出,則可以通過 Find Leaks 菜單快速找出問題。

如下圖所示,展示了名稱叫做 huge-thread 的線程,持有了超過 96% 的對象,數據被一個 HashMap 所持有。

對于特別明顯的內存泄漏,在這里能夠幫助我們迅速定位,但通常內存泄漏問題會比較隱蔽,我們需要更加復雜的分析。
* 2.3. 支配樹視圖
支配樹視圖對數據進行了歸類,體現了對象之間的依賴關系。如圖,我們通常會根據“深堆”進行倒序排序,可以很容易的看到占用內存比較高的幾個對象,點擊前面的箭頭,即可一層層展開支配關系。
圖中顯示的是其中的 1 MB 數據,從左側的 inspector 視圖,可以看到這 1 MB 的 byte 數組具體內容。

從支配樹視圖同樣能夠找到我們創建的兩個循環依賴,但它們并沒有顯示這個過程。

支配樹視圖的概念有一點點復雜,我們只需要了解這個概念即可。

如上圖,左邊是引用關系,右邊是支配樹視圖。可以看到 A、B、C 被當作是“虛擬”的根,支配關系是可傳遞的,因為 C 支配 E,E 支配 G,所以 C 也支配 G。
另外,到對象 C 的路徑中,可以經過 A,也可以經過 B,因此對象 C 的直接支配者也是根對象。同理,對象 E 是 H 的支配者。
我們再來看看比較特殊的 D 和 F。對象 F 與對象 D 相互引用,因為到對象 F 的所有路徑必然經過對象 D,因此,對象 D 是對象 F 的直接支配者。
可以看到支配樹視圖并不一定總是能看到對象的真實應用關系,但對我們分析問題的影響并不是很大。
這個視圖是非常好用的,甚至可以根據 package 進行歸類,對目標類的查找也是非常快捷的。

編譯下面這段代碼,可以展開視圖,實際觀測一下支配樹,這和我們上面介紹的是一致的。
```
public?class?DorminatorTreeDemo?{
????static?class?A?{
????????C?c;
????????byte[]?data?=?new?byte[1024?*?1024?*?2];
????}
????static?class?B?{
????????C?c;
????????byte[]?data?=?new?byte[1024?*?1024?*?3];
????}
????static?class?C?{
????????D?d;
????????E?e;
????????byte[]?data?=?new?byte[1024?*?1024?*?5];
????}
????static?class?D?{
????????F?f;
????????byte[]?data?=?new?byte[1024?*?1024?*?7];
????}
????static?class?E?{
????????G?g;
????????byte[]?data?=?new?byte[1024?*?1024?*?11];
????}
????static?class?F?{
????????D?d;
????????H?h;
????????byte[]?data?=?new?byte[1024?*?1024?*?13];
????}
????static?class?G?{
????????H?h;
????????byte[]?data?=?new?byte[1024?*?1024?*?17];
????}
????static?class?H?{
????????byte[]?data?=?new?byte[1024?*?1024?*?19];
????}
????A?makeRef(A?a,?B?b)?{
????????C?c?=?new?C();
????????D?d?=?new?D();
????????E?e?=?new?E();
????????F?f?=?new?F();
????????G?g?=?new?G();
????????H?h?=?new?H();
????????a.c?=?c;
????????b.c?=?c;
????????c.e?=?e;
????????c.d?=?d;
????????d.f?=?f;
????????e.g?=?g;
????????f.d?=?d;
????????f.h?=?h;
????????g.h?=?h;
????????return?a;
????}
????static?A?a?=?new?A();
????static?B?b?=?new?B();
????public?static?void?main(String[]?args)?throws?Exception?{
????????new?DorminatorTreeDemo().makeRef(a,?b);
????????Thread.sleep(Integer.MAX_VALUE);
????}
}
```

* 2.4. 線程視圖
想要看具體的引用關系,可以通過線程視圖。我們在第 5 講,就已經了解了線程其實是可以作為 GC Roots 的。如圖展示了線程內對象的引用關系,以及方法調用關系,相對比 jstack 獲取的棧 dump,我們能夠更加清晰地看到內存中具體的數據。
如下圖,我們找到了 huge-thread,依次展開找到 holder 對象,可以看到循環依賴已經陷入了無限循環的狀態。這在查看一些 Java 對象的時候,經常發生,不要感到奇怪。

* 2.5. 柱狀圖視圖
我們返回頭來再看一下柱狀圖視圖,可以看到除了對象的大小,還有類的實例個數。結合 MAT 提供的不同顯示方式,往往能夠直接定位問題。也可以通過正則過濾一些信息,我們在這里輸入 MAT,過濾猜測的、可能出現問題的類,可以看到,創建的這些自定義對象,不多不少正好一百個。

右鍵點擊類,然后選擇 incoming,這會列出所有的引用關系。

再次選擇某個引用關系,然后選擇菜單“Path To GC Roots”,即可顯示到 GC Roots 的全路徑。通常在排查內存泄漏的時候,會選擇排除虛弱軟等引用。

使用這種方式,即可在引用之間進行跳轉,方便的找到所需要的信息。

再介紹一個比較高級的功能。
我們對于堆的快照,其實是一個“瞬時態”,有時候僅僅分析這個瞬時狀態,并不一定能確定問題,這就需要對兩個或者多個快照進行對比,來確定一個增長趨勢。

可以將代碼中的 100 改成 10 或其他數字,再次 dump 一份快照進行比較。如圖,通過分析某類對象的增長,即可輔助問題定位。
#### 3. 高級功能—OQL
MAT 支持一種類似于 SQL ?的查詢語言 OQL(Object Query Language),這個查詢語言 VisualVM 工具也支持。

以下是幾個例子,你可以實際實踐一下。
查詢 A4MAT 對象
```
SELECT?*?FROM??Objects4MAT$A4MAT
```
正則查詢 MAT 結尾的對象:
```
SELECT?*?FROM?".*MAT"
```
查詢 String 類的 char 數組:
```
SELECT?OBJECTS?s.value?FROM?java.lang.String?s?
SELECT?OBJECTS?mat.b4MAT?FROM??Objects4MAT$A4MAT?mat
```
根據內存地址查找對象:
```
select?*?from?0x55a034c8
```
使用 INSTANCEOF 關鍵字,查找所有子類:
```
SELECT?*?FROM?INSTANCEOF?java.util.AbstractCollection
```
查詢長度大于 1000 的 byte 數組:
```
SELECT?*?FROM?byte[]?s?WHERE?s.@length>1000
```
查詢包含 java 字樣的所有字符串:
```
SELECT?*?FROM?java.lang.String?s?WHERE?toString(s)?LIKE?".*java.*"
```
查找所有深堆大小大于 1 萬的對象:
```
SELECT?*?FROM?INSTANCEOF?java.lang.Object?o?WHERE?o.@retainedHeapSize>10000
```
如果你忘記這些屬性的名稱的話,MAT 是可以自動補全的。

OQL 有比較多的語法和用法,若想深入了解,可參考這里。
一般,我們使用上面這些簡單的查詢語句就夠用了。
OQL 還有一個好處,就是可以分享。如果你和同事同時在分析一個大堆,不用告訴他先點哪一步、再點哪一步,共享給他一個 OQL 語句就可以了。
如下圖,MAT 貼心的提供了復制 OQL 的功能,但是用在其他快照上,不會起作用,因為它復制的是如下的內容。

#### 4. 小結
這一講我們介紹了 MAT 工具的使用,其是用來分析內存快照的;在最后,簡要介紹了 OQL 查詢語言。
在 Java 9 以前的版本中,有一個工具 jhat,可以以 html 的方式顯示堆棧信息,但和 VisualVm 一樣,都太過于簡陋,推薦使用 MAT 工具。
我們把問題設定為內存泄漏,但其實 OOM 或者頻繁 GC 不一定就是內存泄漏,它也可能是由于某次或者某批請求頻繁而創建了大量對象,所以一些嚴重的、頻繁的 GC 問題也能在這里找到原因。有些情況下,占用內存最多的對象,并不一定是引起內存泄漏問題的元兇,但我們也有一個比較通用的分析過程。
并不是所有的堆都值得分析的,我們在做這個耗時的分析之前,需要有個依據。比如,經過初步調優之后,GC 的停頓時間還是較長,則需要找到頻繁 GC 的原因;再比如,我們發現了內存泄漏,需要找到是誰在搞鬼。
首先,我們高度關注快照載入后的初始分析,占用內存高的 topN 對象,大概率是問題產生者。
對照自己的代碼,首先要分析的,就是產生這些大對象的邏輯。舉幾個實際發生的例子。有一個 Spring Boot 應用,由于啟用了 Swagger 文檔生成器,但是由于它的 API 關系非常復雜,嵌套層次又非常深(每次要產生幾百 M 的文檔!),結果請求幾次之后產生了內存溢出,這在 MAT 上就能夠一眼定位到問題;而另外一個應用,在讀取數據庫的時候使用了分頁,但是 pageSize 并沒有做一些范圍檢查,結果在請求一個較大分頁的時候,使用 fastjson 對獲取的數據進行加工,直接 OOM。
如果不能通過大對象發現問題,則需要對快照進行深入分析。使用柱狀圖和支配樹視圖,配合引入引出和各種排序,能夠對內存的使用進行整體的摸底。由于我們能夠看到內存中的具體數據,排查一些異常數據就容易得多。
可以在程序運行的不同時間點,獲取多份內存快照,對比之后問題會更加容易發現。我們還是用一個例子來看。有一個應用,使用了 Kafka 消息隊列,開了一般大小的消費緩沖區,Kafka 會復用這個緩沖區,按理說不應該有內存問題,但是應用卻頻繁發生 GC。通過對比請求高峰和低峰期間的內存快照,我們發現有工程師把消費數據放入了另外一個 “內存隊列”,寫了一些畫蛇添足的代碼,結果在業務高峰期一股腦把數據加載到了內存中。
上面這些問題通過分析業務代碼,也不難發現其關聯性。問題如果非常隱蔽,則需要使用 OQL 等語言,對問題一一排查、確認。
可以看到,上手 MAT 工具是有一定門檻的,除了其操作模式,還需要對我們前面介紹的理論知識有深入的理解,比如 GC Roots、各種引用級別等。
在很多場景,MAT 并不僅僅用于內存泄漏的排查。由于我們能夠看到內存上的具體數據,在排查一些難度非常高的 bug 時,MAT 也有用武之地。比如,因為某些臟數據,引起了程序的執行異常,此時,想要找到它們,不要忘了 MAT 這個老朋友。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充