工具概述
使用前面的命令行能夠獲取目標Java應用性能相關的基礎信息,但它們存在下列局限:
- 無法獲取方法級別的分析數據,如方法間的調用關系、各方法的調用次數和調用時間等(這對定位應用性能瓶頸至關重要)。
- 要求用戶登錄到目標Java應用所在的宿主機上,使用起來不是很方便。
- 分析數據通過終端輸出,結果展示不夠直觀。
為此,JDK提供了一些內存泄漏的分析工具,如jconsole、jvisualVM等,用於輔助開發人員定位問題,但是這些工具很多時候並不足以滿足快速定位的需求,所以這里我們介紹的工具相對多一些、豐富一些。
圖形化綜合診斷工具
- JDK自帶的工具
- jconsole:JDK自帶的可視化監控工具。查看Java應用程序的運行概況、監控堆信息、永久區(或元空間)使用情況、類加載情況等。位置:jdk\bin\jconsole.exe
- visual VM:visual vm是一個工具,它提供了一個可視界面,用於查看Java虛擬機上運行的基於Java技術的應用程序的詳細信息。位置:jdk\bin\jvisualvm.exe
- JMC:Java Mission Control,內置Java Flight Recorder。能夠以極低的性能開銷收集Java虛擬機的性能數據。
- 第三方工具
- MAT:MAT(Memory Analyzer Tool)是基於Eclipse的內存分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。Eclipse的插件形式。
- JProfiler:商業軟件,需要付費,功能強大。與VisualVM類似
- Arthas:Alibaba開源的Java診斷工具,深受開發者喜愛。
- Btrace:Java運行時追蹤工具。可以在不停機的情況下,跟蹤指定的方法調用、構造函數調用和系統內存等信息。
jconsole
JConsole,顧名思義,就是“Java 控制台”,從Java5 開始,在JDK中自帶的java監控和管理控制台。用於對JVM中內存、線程和類等的監控,是一個基於JMX(java management extensions)的GUI性能監控圖形界面工具。主要是 3 款:JConsole、JVisualVM、JMC。這三個工具都支持我們分析本地 JVM 進程,或者通過 JMX 等方式連接到遠程 JVM 進程。當然,圖形界面工具的版本號和目標 JVM 不能差別太大,否則可能會報錯。在這里,我們可以從多個維度和時間范圍去監控一個 Java 進程的內外部指標。進而通過這些指標數據來分析判斷 JVM 的狀態,為我們的調優提供依據。
三種連接方式
- Loca:使用JConsole連接一個正在本地系統運行的JVM,並且執行程序的和運行JConsole的需要是同一個用戶。JConsole使用文件系統的授權通過RMI連接器連接到平台的MBean服務器上。這種從本地連接的監控能力只有Sun的JDK具有。
- Remote:使用下面的URL通過RMI連接器連接到一個JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole為建立連接,需要在環境變量中設置mx.remote.credentials來指定用戶名和密碼,從而進行授權。
- Advanced:使用一個特殊的URL連接JMX代理。一般情況使用自己定制的連接器而不是RMI提供的連接器來連接JMX代理,或者是一個使用JDK1.4的實現了JMX和JMX Rmote的應用。
在 Windows 或 macOS 的運行窗口或命令行輸入 jconsole,然后回車,可以看到如下界面:
本地進程列表列出了本機的所有 Java 進程(遠程進程后續講解),選擇一個要連接的 Java 進程,點擊連接,然后可以看到如下界面:
注意,點擊右上角的綠色連接圖標,即可連接或斷開這個 Java 進程。
上圖中顯示了總共 6 個標簽頁,每個標簽頁對應一個監控面板,分別為:
- 概覽:以圖表方式查看 Java 進程的堆內存、線程、類、CPU 占用率四項指標和歷史。
- 內存:JVM 的各個內存池的使用情況以及明細。
- 線程:JVM 內所有的線程列表和具體的狀態信息。
- 類:JVM 加載和卸載的類數量匯總信息。
- VM 概要:JVM 的供應商、運行時間、JVM 參數,以及其他數據的摘要。
- MBean:跟 JMX 相關的 MBean,后續講解。
概覽
概覽信息見上圖,四項指標具體為:
- 堆內存使用量:此處展示的就是前面 Java 內存模型課程中提到的堆內存使用情況,從圖上可以看到,堆內存使用了 4MB 左右,並且一直在增長。
- 線程:展示了 JVM 中活動線程的數量,當前時刻共有 14 個活動線程。
- 類:JVM 一共加載了 1744 個類,沒有卸載類。
- CPU 占用率:目前 CPU 使用率為 0.0%,這個數值非常低,且最高的時候也不到 2%,初步判斷系統當前並沒有什么負載和壓力。
在概覽面板中,我們可以看到從 JConsole 連接到 Java 進程之后的所有數據。但是如果從連接進程到現在的時間很長,比如 2 天,那么這里的圖表就因為要在一個界面展示而擠壓到一起,歷史的數據被平滑處理了,當前的變化細節就看不清楚。
所以,JConsole 提供了多個時間范圍供我們選擇,點擊時間范圍后面的下拉列表,即可查看不同區間的數據。有如下幾個時間維度可供選擇:1 分鍾、5 分鍾、10 分鍾、30 分鍾、1 小時、2 小時、3 小時、6小時、12 小時、1 天、7 天、1 個月、3 個月、6 個月、1 年、全部,一共是 16 檔。
當我們想關注最近 1 小時或者 1 分鍾的數據,就可以選擇對應的檔。旁邊的 3 個標簽頁(內存、線程、類),也都支持選擇時間范圍。
內存
內存監控,是 JConsole 中最常用的面板。內存面板的主區域中展示了內存占用量隨時間變化的圖像,可以通過這個圖表,非常直觀地判斷內存的使用量和變化趨勢。
同時在左上方,我們可以在圖表后面的下拉框中選擇不同的內存區:
本例中,我們使用的是 JDK 8,默認不配置 GC 啟動參數。可以看到,這個 JVM 提供的內存圖表包括:
- 堆內存使用量,主要包括老年代(內存池“PS Old Gen”)、新生代(“PS Eden Space”)、存活區(“PS Survivor Space”);
- 非堆內存使用量,主要包括內存池“Metaspace”、“Code Cache”、“Compressed Class Space”等;
- 可以分別選擇對應的 6 個內存池。
通過內存面板,我們可以看到各個區域的內存使用和變化情況,並且可以:
- 手動執行 GC,點擊按鈕即可執行 JDK 中的 System.gc(),直接觸發 GC 操作,一般來說,除非啟動時明確指定了禁止手動 GC,否則 JVM 都會立刻執行 FullGC;
- 通過圖中右下角的界面,可以看到各個內存池的百分比使用率,以及堆/非堆空間的匯總使用情況,這個圖會實時變化,同時可以直接點擊這里的各個部分快速切換上方圖表,顯示對應區域的內存使用情況;
- 從左下角的界面,可以看到 JVM 使用的垃圾收集器,以及執行垃圾收集的次數,以及相應的時間消耗。
打開一段時間以后,我們可以看到內存使用量出現了波峰曲線,只要曲線出現了下降就表明經過了一次 GC,也就是 JVM 執行了垃圾回收。
其實我們可以注意到,內存面板其實相當於是 jstat -gc
或 jstat -gcutil
命令的圖形化展示,它們的本質是一樣的,都是通過采樣的方式拿到JVM各個內存池的數據進行統計,並展示出來。
其實圖形界面存在一個問題,如果 GC 特別頻繁,每秒鍾執行了很多次 GC,實際上圖表方式就很難反應出每一次的變化信息。
線程
線程面板展示了線程數變化信息,以及監測到的線程列表。
- 我們可以常根據名稱直接查看線程的狀態(運行還是等待中)和調用棧(正在執行什么操作)。
- 特別地,我們還可以直接點擊“檢測死鎖”按鈕來檢測死鎖,如果沒有死鎖則會提示“未檢測到死鎖”。
類
類監控面板,可以直接看到 JVM 加載和卸載的類數量匯總信息。
VM 概要
VM 概要的數據也很有用,可以看到總共有五個部分:
- 第一部分是虛擬機的信息;
- 第二部分是線程數量,以及類加載的匯總信息;
- 第三部分是堆內存和 GC 統計;
- 第四部分是操作系統和宿主機的設備信息,比如 CPU 數量、物理內存、虛擬內存等等;
- 第五部分是 JVM 啟動參數和幾個關鍵路徑,這些信息其實跟 jinfo 命令看到的差不多。
JVisualVM 圖形界面監控工具
Visual vm是一個功能強大的多合一故障診斷和性能監控的可視化工具。它集成了多個JDK命令行工具,使用visual vm可用於顯示虛擬機進程及進程的配置和環境信息(jps、jinfo),監視應用程序的CPU、GC、堆、方法區及線程的信息(jstat、jstack)等,甚至代替Jconsole。在JDK 6 Update 7 以后,Visual VM便成為JDK的一部分發布。此外,Visual VM也可以作為獨立的軟件安裝。
連接方式
- 本地連接:監控本地Java進程的CPU、類、線程等
- 遠程連接:
- 確定遠程服務器的ip地址
- 添加JMX(通過JMX技術具體監控遠端服務器的某個Java進程)
- 修改bin/catalina.sh文件,連接遠程的tomcat
- 在…/conf中添加jmxremote.access和jmxremote.password文件
- 將服務器地址改成公網ip地址
- 設置阿里雲安全策略和防火牆策略
- 啟動tomcat,查看tomcat啟動日志和端口監聽
- JMX中輸入端口號、用戶名、密碼登錄
主要功能
- 生成/讀取堆內存快照
- 查看JVM參數和系統屬性
- 查看運行中的虛擬機進程
- 生成/讀取線程快照
- 程序資源的實時監控
- 其他功能:JMX代理連接,遠程環境監控,CPU分析和內存分析
在命令行或者運行窗口直接輸入 jvisualvm 即可啟動,JVisualVM 啟動后的界面大致如下:
在其中可以看到本地的 JVM 實例。
通過雙擊本地進程或者右鍵打開,就可以連接到某個 JVM,此時顯示的基本信息如下圖所示:
可以看到,在概述頁簽中有 PID、啟動參數、系統屬性等信息。
切換到監視頁簽:
如果沒有監視頁簽選項,需要安裝插件,但是默認的下載地址不對,需要配置,修改的具體地址可以在下面的地址中找到https://visualvm.github.io/pluginscenters.html
修改以后,就可以看見能安裝的插件列表
現在點開監視頁簽
在監視頁簽中可以看到 JVM 整體的運行情況。比如 CPU、堆內存、類、線程等信息。還可以執行一些操作,比如“強制執行垃圾回收”、“堆 Dump”等。
"線程"頁簽則展示了 JVM 中的線程列表。
與 JConsole 只能看線程的調用棧和狀態信息相比,這里可以直觀看到所有線程的狀態顏色和運行時間,從而幫助我們分析過去一段時間哪些線程使用了較多的 CPU 資源。
抽樣器與 Profiler
JVisualVM 默認情況下,比 JConsole 多了抽樣器和 Profiler 這兩個工具。
例如抽樣,可以配合我們在性能壓測的時候,看壓測過程中,各個線程發生了什么、或者是分配了多少內存,每個類直接占用了多少內存等等。
使用 Profiler 時,需要先校准分析器。
然后可以像抽樣器一樣使用了。
Profiler 面板直接能看到熱點方法與執行時間、占用內存以及比例,還可以設置過濾條件。
同時我們可以直接把當前的數據和分析,作為快照保存,或者將數據導出,以后可以繼續加載和分析。
VisualGC 頁簽:
在其中可以看到各個內存池的使用情況,以及類加載時間、GC 總次數、GC 總耗時等信息。比起命令行工具要簡單得多。
MBeans 標簽:
主要看 java.lang 包下面的 MBean。比如內存池或者垃圾收集器等。
從圖中可以看到,Metaspace 內存池的 Type 是 NON_HEAP。
當然,還可以看垃圾收集器(GarbageCollector)。
對所有的垃圾收集器,通過 JMX API 獲取的信息包括:
- CollectionCount:垃圾收集器執行的 GC 總次數。
- CollectionTime:收集器運行時間的累計,這個值等於所有 GC 事件持續時間的總和。
- LastGcInfo:最近一次 GC 事件的詳細信息。包括 GC 事件的持續時間(duration)、開始時間(startTime)和結束時間(endTime),以及各個內存池在最近一次 GC 之前和之后的使用情況。
- MemoryPoolNames:各個內存池的名稱。
- Name:垃圾收集器的名稱。
- ObjectName:由 JMX 規范定義的 MBean 的名字。
- Valid:此收集器是否有效。本人只見過 "true" 的情況。
這些信息對分析GC性能來說,不能得出什么結論。只有編寫程序,獲取GC相關的 JMX 信息來進行統計和分析。
下面看怎么執行遠程實時監控。
如上圖所示,從文件菜單中,我們可以選擇“添加遠程主機”,以及“添加 JMX 連接”。
比如“添加 JMX 連接”,填上 IP 和端口號之后,勾選“不要求 SSL 連接”,點擊“確定”按鈕即可。
關於目標 JVM 怎么啟動 JMX 支持,后續再討論。
遠程主機則需要 JStatD 的支持,后續再討論。
MAT(Memory Analyzer Tool)
一款功能強大的Java堆內存分析器。可以用來查找內存泄漏以及查看內存消耗情況。MAT是基於Eclipse開發的,不僅可以單獨使用,還可以作為插件的形式嵌入在Eclipse中使用。是一款免費的性能分析工具,使用起來非常方便。
獲取堆dump文件
dump文件內容
MAT可以分析heap dump文件。在進行內存分析時,只要獲得了反映當前設備內存映像的hprof文件,通過MAT打開就可以直觀地看到當前的內存信息。
一般說來,這些內存信息包含:
- 所有的對象信息,包括對象實例、成員變量、存儲於棧中的基本類型值和存儲於堆中的其他對象的引用值。
- 所有的類信息,包括classloader、 類名稱、父類、靜態變量等
- GCRoot到所有的這些對象的引用路徑
- 線程信息,包括線程的調用棧及此線程的線程局部變量(TLS)
說明:
- MAT 不是一個萬能工具,它並不能處理所有類型的堆存儲文件。但是比較主流的廠家和格式,例如 Sun, HP, SAP所采用的HPROF 二進制堆存儲文件,以及IBM的PHD堆存儲文件等都能被很好的解析。
- 最吸引人的還是能夠快速為開發人員生成內存泄漏報表,方便定位問題和分析問題。雖然MAT有如此強大的功能,但是內存分析也沒有簡單到-鍵完成的程度,很多內存問題還是需要我們從MAT展現給我們的信息當中通過經驗和直覺來判斷才能發現。
獲取dump文件
- 通過前文介紹的jmap工具生成,可以生成任意-一個java進程的dump文件;
- 通過配置JVM參數生成。
- 選項"-XX:+HeapDumpOnOutOfMemoryError"或"-XX:+HeapDumpBeforeFullGC"
- 選項"-XX:HeapDumpPath"所代表的含義就是當程序出現0utofMemory時,將會在相應的目錄下 生成一份dump文件。如果不指定選項“-XX:HeapDumpPath" 則在當前目錄下生成dump文件。
- 對比:考慮到生產環境中幾乎不可能在線對其進行分析,大都是采用離線分析,因此使用jmap+MAT工具是最常見的組合。
- 使用VisualVM可以導出堆dump文件
- 使用MAT既可以打開一個已有的堆快照,也可以通過MAT直接從活動Java程序中導出堆快照。 該功能將借助jps列出當前正在運行的Java 進程,以供選擇並獲取快照。
分析堆dump文件
如果dump文件過大,可以修改 MAT 的配置參數。在 MAT 安裝目錄下,修改配置文件:MemoryAnalyzer.ini。默認的內存配置是 1024MB,修改如下部分:
-vmargs
-Xmx1024m
根據 Dump 文件的大小,適當增加最大堆內存設置,要求是 4MB 的倍數,例如改為:
-vmargs
-Xmx4g
demo演示
新建demo:
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(); } }
代碼創建了一個新的線程 "huge-thread",並建立了一個引用的層級關系,總的內存大約占用 100 MB。同時,demo1 和 demo2 展示了一個循環引用的關系。最后,使用 sleep 函數,讓線程永久阻塞住,此時整個堆處於一個相對“靜止”的狀態。
運行上面代碼,並通過jps獲取進程號,接着執行以下命令行獲取dump文件。
jmap -dump:format=b,file=d:\test\a.hprof 25876
通過MAT打開a.hprof文件。
這種方式對應的是獲取dump文件的方法一。
也可以通過方法四獲取:
選擇指定進程生成dump文件
如果問題特別突出,則可以通過 Find Leaks 菜單快速找出問題。
如下圖所示,展示了名稱叫做 huge-thread 的線程,持有了超過 99% 的對象,數據被一個 HashMap 所持有。
1.histogram:
展示了各個類的實例數目以及這些實例的Shallow heap或者Retained heap的總和,並支持基於實例數目或 Retained heap 的排序方式(默認為 Shallow heap)。此外,還可以將直方圖中的類按照超類、類加載器或者包名分組。
2.thread overview:
查看系統中的Java線程,查看局部變量的信息
如圖展示了線程內對象的引用關系,以及方法調用關系,相對比 jstack 獲取的棧 dump,我們能夠更加清晰地看到內存中具體的數據。而且,我們找到了 huge-thread,依次展開找到 holder 對象,可以看到循環依賴已經陷入了無限循環的狀態。這在查看一些 Java 對象的時候,經常發生。
3.獲得對象相互引用的關系
with outgoing references(對象的引出) 和 with incoming reference(對象的引入)
path to GC Roots 顯示和 GC Roots 之間的路徑。
4.淺堆和深堆
MAT 計算對象占據內存的兩種方式。shallow heap和retained heap。
淺堆(shallow heap)是指一個對象所消耗的內存,包括對象自身的內存占用,以及“為了引用”其他對象所占用的內存。。
深堆(Retained Heap)是指對象的保留集中所有的對象的淺堆大小之和。
注意:淺堆指對象本身占有的內存,不包括其內部引用對象的大小,一個對象的深堆指只能通過該對象訪問到的(直接或間接)所有對象的淺堆之和,即對象被回收,可以釋放的總內存,包括對象自身所占據的內存,以及僅能夠通過該對象引用到的其他對象所占據的內存,這些其他對象集合,叫做保留集(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 對象淺堆。
一個對象占用的內存大小可以分為下面三部分:
- 對象頭
- 對象成員占用內存
- 內存對齊
對象頭主要分為三部分:
運行時元數據:這個主要保存了對象的哈希值、GC分代年齡、鎖信息等等占用8個字節。
類型指針:指向方法區該類的Klass
數組長度:如果當前對象是數組類型的,那么還會擁有4字節的數組長度
那么對象頭的大小=8 + 指針大小 + [4:如果是數組的話]
在內存小於32G的情況下,我們默認采用了壓縮指針,指針長度為32位。我們也可以禁用壓縮指針,那么指針長度將為64位。一般我們個人開發的情況下,內存基本都小於32G,所以我們可以認為普通對象的對象頭大小為12字節,數組為16字節。
在32位系統中,一個對象引用會占用4個字節,一個int類型會占用4個字節,long類型會占用8個字節,每個對象頭會占用12或16個字節。根據堆快照格式不同,對象的大小可能會向8字節進行對齊。對象頭除去類型指針的大小為8字節,然后類型指針看是否啟用了引用壓縮,如果啟用了,對象頭總共就是12字節,否則就是16字節。
以String為例:1個int值共占4個字節,對象引用占用4個字節,對象頭12個字節,合計20個字節。向8字節對齊,故占24字節。這24 字節為String對象的淺堆大小。它與String的value實際取值無關,無論字符串長度如何,淺堆大小始終是24字節。
保留集(Retained Set):
對象A的保留集指當對象A被垃圾回收后,可以被釋放的所有的對象集合(包括對象A本身),即對象A的保留集可以被認為是只能通過對象A被直接或間接訪問到的所有對象的集合。通俗地說,就是指僅被對象A所持有的的對象的集合。
MAT 包括了兩個比較重要的視圖,分別是直方圖(histogram)和支配樹(dominator tree)。
支配樹(Dominator Tree)
MAT提供了一個稱為支配樹的對象圖。支配樹視圖對數據進行了歸類,體現了對象實例間的支配關系。在對象引用圖中,所有指向對象B的路徑都經過對象A ,則認為對象A支配了對象B。如果對象A時離對象B最近的一個支配對象,則認為對象A為對象B的直接支配者,支配樹是基於對象間的引用圖所建立的,它有以下基本性質:
- 對象A的子樹(所有被對象A支配的對象集合)表示對象A的保留集(retained set),即深堆。
- 如果對象A支配對象B,那么對象A的直接支配者也支配對象B。
- 支配樹的邊與對象引用圖的邊不直接對應。
如下圖所示:左圖表示對象引用圖,右圖表示左圖所對應的支配樹。對象A和B由根對象直接支配,由於在到對象C的路徑中,可以經過A,也可以經過B,因此對象C的直接支配者也是根對象。對象F與對象D相互引用,因為到對象F的所有路徑必然經過對象D,因此,對象D時對象F的直接支配者。而到對象D的所有路徑中,必然經過對象C,即使是從對象F到對象D的引用,從根節點觸發,也是經過對象C的,所以,對象D的直接支配者為對象C。
同理,對象E支配對象G,支配關系是可傳遞的,因為 C 支配 E,所以 C 也支配 G。到達對象H可以通過對象D,也可以通過對象E,因此對象D和E都不能支配對象H,而經過對象C既可以到達D也可以到達E,因此對象C為對象H的直接支配者。
MAT支配樹視圖
如圖,我們通常會根據“深堆”進行倒序排序,可以很容易的看到占用內存比較高的幾個對象,點擊前面的箭頭,即可一層層展開支配關系。
圖中顯示的是其中的 2 MB 數據,從左側的 inspector 視圖,可以看到這 2 MB 的 byte 數組具體內容。
從支配樹視圖同樣能夠找到我們創建的兩個循環依賴,但它們並沒有顯示這個過程。
直方圖(histogram)
看一下柱狀圖視圖,可以看到除了對象的大小,還有類的實例個數。結合 MAT 提供的不同顯示方式,往往能夠直接定位問題。也可以通過正則過濾一些信息,我們在這里輸入 MAT,過濾猜測的、可能出現問題的類,可以看到,創建的這些自定義對象,不多不少正好一百個。
右鍵點擊類,然后選擇 incoming,這會列出所有的引用關系。
再次選擇某個引用關系,然后選擇菜單“Path To GC Roots”,即可顯示到 GC Roots 的全路徑。通常在排查內存泄漏的時候,會選擇排除虛弱軟等引用。使用這種方式,即可在引用之間進行跳轉,方便的找到所需要的信息。
再介紹一個比較高級的功能。
我們對於堆的快照,其實是一個“瞬時態”,有時候僅僅分析這個瞬時狀態,並不一定能確定問題,這就需要對兩個或者多個快照進行對比,來確定一個增長趨勢。
可以將代碼中的 100 改成 10 或其他數字,再次 dump 一份快照進行比較。
OQL
MAT 支持一種類似於 SQL 的查詢語言 OQL(Object Query Language),這個查詢語言 VisualVM 工具也支持。
以下是幾個例子
查詢 A4MAT 對象:
SELECT * FROM com.xiaojie.jvm.Objects4MAT$A4MAT
正則查詢 MAT 結尾的對象:
SELECT * FROM ".*MAT"
查詢包含 java 字樣的所有字符串:
SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*java.*"
查找所有深堆大小大於 1 萬的對象:
SELECT * FROM INSTANCEOF java.lang.Object o WHERE o.@retainedHeapSize>10000
分析對象大小demo演示
代碼
public class StudentTrace {
static List<WebPage> webPages = new ArrayList<>(); public static void createWebPages() { for (int i = 0; i < 100; i++) { WebPage webPage = new WebPage(); webPage.setUrl("www." + Integer.toString(i) + ".com"); webPage.setContent(Integer.toString(i)); webPages.add(webPage); } } public static void main(String[] args) { createWebPages(); Student student3 = new Student(3, "張三"); Student student5 = new Student(5, "李四"); Student student7 = new Student(7, "王五"); for (int i = 0; i < webPages.size(); i++) { if (i % student3.getId() == 0) { student3.visit(webPages.get(i)); } if (i % student5.getId() == 0) { student5.visit(webPages.get(i)); } if (i % student7.getId() == 0) { student7.visit(webPages.get(i)); } } webPages.clear(); System.gc(); } } class Student { private int id; private String name; private List<WebPage> history = new ArrayList<>(); public Student(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<WebPage> getHistory() { return history; } public void setHistory(List<WebPage> history) { this.history = history; } public void visit(WebPage webPage) { if (webPage != null) { history.add(webPage); } } } class WebPage { private String url; private String content; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
vm options配置:
-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\tmp\b.hprof
利用MAT打開b.hprof
三個對象的淺堆大小為24字節,因為兩個引用占用8字節,一個int占用4字節,對象頭12字節,總共24字節。
每個student對象的網頁集合字段中的每個對象所占用的深堆大小為152和144。
1.為什么有152字節和144字節:因為我們的URL和content存在兩種情況
URL:"http://www.7.com"、content:"7"
URL:"http://www.14.com"、content:"14"
第一種URL長度為16,底層的char數組的占用空間為(【】方括號里面整個都屬於對象頭,分開寫方便大家理解)
【普通對象頭(12) + 數組長度(4)】 + 16個字符(32) = 48字節,符合8字節對齊
同理content 占用 【普通對象頭(12) +數組長度(4)】+ 一個字符(2) = 18字節,八字節對齊=24字節
第二種URL長度為17,底層的插入數組的占用空間為
【普通對象頭(12) + 數組長度(4)】 + 17個字符(34) = 50字節,不符合8字節對齊,對齊為56
同理content 占用 【普通對象頭(12) +數組長度(4)】+ 兩個字符(4) = 20字節,八字節對齊=24字節
所以第一種總字節為48 + 24 = 72,第二種總字節為56 + 24 = 80。因此第二種比第一種多了8字節,所以是152和144。
(為什么總大小是152而不是72是因為我們只計算了String底層的char數組的區別沒有計算各變量本身的淺堆,
因為結構都想相同,所以差別就差在內容的占用上)
2.為什么最終結果是1288
首先ElementData數組本身的淺堆大小為
【普通對象頭(12) + 數組長度(4)】 + 數組內容【15個Obejct引用=16*4】 = 76,八字節對齊=80字節
15個Object分為13個152字節+2個144字節,總大小為=2264字節
7號和其他student重復的有0、21、42、63、84、35、70總計6個152和1一個144
所以2264 - 6 * 152 - 144 = 1208字節
所以ElementData本身的淺堆80 + 僅能通過它到達的淺堆1208 = 1288
JProfiler
JProfiler數據采集方式分為兩種:Sampling(樣本采集)和 Instrumentation(重構模式)
- Instrumentation:這是JProfiler全功能模式。在class加載之前,JProfiler吧相關功能代碼寫入到需要分析的class的bytecode中,對正在運行的JVM有一定影響。
- 優點:功能強大。在此設置中,調用堆棧信息時准確的。
- 缺點:若要分析的class較多,則對應用的性能影響較大,CPU開銷可能很高(取決於Filter的控制)。因此使用此模式一般配合Filter使用,只對特定的類或包進行分析。
- Sampling:類似樣本統計,每個一定時間(5ms)將每個線程棧中的信息統計出來。
- 優點:對CPU的開銷非常低,對應用影響小(即使不配置任何Filter)
- 缺點:一些數據、特性不能提供(例如:方法的調用次數、執行時間)
注意:JProfiler本身沒有指出數據的采集類型,這里的采集類型是針對方法調用的采集類型。因為JProfiler的絕大多數核心功能都依賴方法調用采集的數據,所以可以直接認為是JProfiler的數據采集類型。
Telemetries(遙感監測)
Live Memory(內存視圖)
Live memory 內存剖析:class、class instance的相關信息。例如對象的個數,大小,對象創建的方法執行棧,對象創建的熱點。
- ALL Objects(所有對象) :顯示所有加載的類的列表和在堆上分配的實例數。
- Record Objects(記錄對象):查看特定時間段對象的分配,並記錄分配的調用堆棧。
- Allocation Call Tree(分配訪問樹):顯示一顆請求樹或者方法、類、包或對已選擇類由帶注釋的分配信息的J2EE組件。
- Allocation Hot Spots(分配熱點):顯示一個列表,包括方法、類、包或分配已選類的J2EE組件。你可以標注當前值並且顯示差異值。對於每個熱點都可以顯示它的跟蹤記錄樹。
- Class Tracker(類追蹤器):類追蹤視圖可以包含任意數量的圖表,顯示特定的類型和包的實例和時間。
注意:
- All Objects后面的Size大小是淺堆大小
- Record Objects在判斷內存泄露的時候使用,可以通過觀察Telemetries中的Memory,如果里面出現垃圾回收之后的內存占用逐步提高,這就有可能出現內存泄露問題,所以可以使用Record Objects查看,但是該分析默認不開啟,畢竟占用CPU性能太多。
堆遍歷
如果通過內存視圖 Live Memory已經分析出哪個類的對象不能進行垃圾回收,並且有可能導致內存溢出,如果想進一步分析,我們可以在該對象上點擊右鍵,選擇Show Selection In Heap Walker,如下圖:
之后進行溯源,操作如下:
在Picture上雙擊左鍵,或者單擊右鍵之后選擇 Select Object,之后選擇reference,里面我們用到outgoing reference,這個就是找我們會用到誰,而incoming reference 是找誰用了我們。
查看結果,並根據結果去看對應的圖表:
也可以點擊show inGraph 查看圖表
以下是圖表的展示情況:
CPU視圖
方法統計
具體分析
可以用來查看方法直接的調用情況,上面的100.0%代表該方法會100.0%調用下面方法,637ms代表調用時間花費637ms,1inv代表調用下面方法1次。
線程視圖 threads
JProfiler通過對線程歷史的監控判斷其運行狀態,並監控是否有線程阻塞產生,還能將一個線程所管理的方法以樹狀形式呈現。對線程剖析。
線程歷史 Thread History
顯示一個與線程活動和線程狀態在一起的活動時間表。
線程監控 Thread Monitor
顯示一個列表,包括所有的活動線程以及它們目前的活動狀況。
線程轉儲 Thread Dumps
顯示所有線程的堆棧跟蹤。
線程分析主要關心三個方面:
- web容器的線程最大數。比如:Tomcat的線程容量應該略大於最大並發數
- 線程阻塞
- 線程死鎖
監視器&鎖 Monitors&locks
Arthas
Arthas(阿爾薩斯)是阿里巴巴推出了一款開源的 Java 診斷工具,深受開發者喜愛。為什么這么說呢?
- Arthas 支持 JDK 6 以及更高版本的 JDK;
- 支持 Linux/Mac/Winodws 操作系統;
- 采用命令行交互模式,同時提供豐富的 Tab 自動補全功能,方便進行問題的定位和診斷;
- 支持 WebConsole,在某些復雜的情況下,打通 HTTP 路由就可以訪問。
當我們遇到以下類似問題而束手無策時,可以使用 Arthas 來幫助我們解決:
- 這個類從哪個 jar 包加載的?為什么會報各種類相關的 Exception?
- 我改的代碼為什么沒有執行到?難道是我沒 commit?分支搞錯了?
- 遇到問題無法在線上 Debug,難道只能通過加日志再重新發布嗎?
- 線上遇到某個用戶的數據處理有問題,但線上同樣無法 debug,線下無法重現!
- 是否有一個全局視角來查看系統的運行狀況?
- 有什么辦法可以監控到 JVM 的實時運行狀態?
- 怎么快速定位應用的熱點,生成火焰圖?
官方文檔:https://arthas.aliyun.com/doc/
安裝
安裝方式一:可以直接在linux上通過命令下載
# 准備目錄 mkdir -p /usr/local/tools/arthas cd /usr/local/tools/arthas # 執行安裝腳本 curl -L https://alibaba.github.io/arthas/install.sh | sh ······
可以在官方Github上進行下載,也可以通過Gitee下載
- github:wget https://alibaba.github.io/arthas/arthas-boot.jar
- gitee:wget https://arthas.gitee.io/arthas-boot.jar
安裝方式二:本地訪問 https://alibaba.github.io/arthas/arthas-boot.jar,下載成功后,上傳到Linux服務器。
啟動
方式一:直接使用 java -jar arthas-boot.jar 啟動
選擇進程(輸入[]內編號,回車)
方式二:運行時選擇Java進程PID : java -jar arthas-boot.jar [PID]
除了在命令行查看外,也可以通過web頁面訪問,http://127.0.0.1:3658/,操作模式和控制台一樣。
指令
help:查看命令幫助信息,可以查看當前arthas版本支持的指令,或者查看具體指令的使用說明。
cat:打印文件內容,和linux里的cat命令類似。
echo:打印參數,和linux里的echo命令類似。
jvm:查看當前JVM信息。
dashboard:當前系統的實時數據面板。
JMC 圖形界面客戶端
Java Mission Control(JMC)是 Java 虛擬機平台上的性能監控工具。它包含一個 GUI 客戶端,以及眾多用來收集 Java 虛擬機性能數據的插件,如 JMX Console(能夠訪問用來存放虛擬機各個子系統運行數據的MXBeans),以及虛擬機內置的高效 profiling 工具 Java Flight Recorder(JFR)。
JMC 和 JVisualVM 功能類似,因為 JMC 的前身是 JRMC,JRMC 是 BEA 公司的 JRockit JDK 自帶的分析工具,被 Oracle 收購以后,整合成了 JMC 工具。Oracle 試圖用 JMC 來取代 JVisualVM,在商業環境使用 JFR 需要付費獲取授權。
在命令行輸入 jmc 后,啟動后的界面如下:
點擊相關的按鈕或者菜單即可啟用對應的功能,JMC 提供的功能和 JVisualVM 差不多。
飛行記錄器
除了 JConsole 和 JVisualVM 的常見功能(包括 JMX 和插件)以外,JMC 最大的亮點是飛行記錄器。
這里需要注意的一點是,JMC可以用於java7以上的所有版本,而飛行記錄器,只能用於oracle jre,且是java7及以上的版本,因為要使用飛行記錄器,需要開啟jvm的商業特性,也就是在啟動的時候加上參數:"-XX:+UnlockCommercialFeatures","-XX:+FlightRecorder"。如果是open jdk,嘗試加這兩個參數的時候,會直接導致虛擬機終止,無法正常啟動。所以,飛行記錄器只能局限在oracle jdk里面使用。這里就不展示了,自行網上研究下。
JStatD 服務端工具
JStatD 是一款強大的服務端支持工具,用於配合遠程監控jvm的創建和結束,並且提供接口讓監控工具可以遠程連接到本機的jvm 。JStatD 位於 $JAVA_HOME/bin目錄下,具體使用方法如下:
1,啟動RMI服務
在需要被監控的服務器上面,通過jstatd來啟動RMI服務。但因為涉及暴露一些服務器信息,所以需要配置安全策略文件。
- 配置java安全訪問,在jdk的bin目錄下創建文件jstatd.all.policy
- 寫入下面的安全配置
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};
然后在進入jstatd.all.policy所在目錄下,通過如下的命令啟動RMI服務:
jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=192.168.211.132 &
其中 192.168.211.132 是公網 IP,如果沒有公網,那么就是內網 IP。
驗證是否啟動成功
服務器:
客戶端:
若出現以上文案,則啟動成功
2.然后使用 JVisualVM 或者 JConsole 連接遠程服務器。
其中 IP 為 192.168.211.132,端口號是默認的 1099。當然,端口號可以通過參數自定義。
說明:客戶端與服務器的 JVM 大版本號必須一致或者兼容。
CPU 圖形沒有顯示,原因是 JStatD 不監控單個實例的 CPU。可以在對應 Java 應用的啟動參數中增加 JMX 監控配置。
BTrace 診斷分析工具
BTrace 是基於 Java 語言的一款動態追蹤工具,可用於輔助問題診斷和分析。BTrace 基於 ASM、Java Attach API、Instruments 開發,提供很多注解。通過這些注解,可以通過 Java 代碼來編寫 BTrace 腳本進行只讀監控,而無需深入了解 ASM 對字節碼的操縱。
BTrace 項目地址:https://github.com/btraceio/btrace/
下面我們來實際操作一下。
BTrace 下載
找到 Release 頁面,找到最新的壓縮包下載:
下載完成后解壓即可使用:
可以看到,bin 目錄下是可執行文件,samples 目錄下是腳本示例。
示例程序
我們先編寫一個有入參有返回值的方法,示例如下:
public class RandomSample { public static void main(String[] args) throws Exception { // int count = 10000; int seed = 0; for (int i = 0; i < count; i++) { seed = randomHash(seed); TimeUnit.SECONDS.sleep(2); } } public static int randomHash(Integer seed) { String uuid = UUID.randomUUID().toString(); int hashCode = uuid.hashCode(); System.out.println("prev.seed=" + seed); return hashCode; } }
運行程序,可以看到控制台每隔一段時間就有一些輸出:
prev.seed=0 prev.seed=-1498044692 prev.seed=-266090177 prev.seed=-1269488296 prev.seed=-354526660 prev.seed=1226660026 prev.seed=662501151 prev.seed=-917015412 prev.seed=743781789 prev.seed=840693320 prev.seed=1161830176 prev.seed=-517897036 prev.seed=150130649 prev.seed=-1379375222 prev.seed=-439945231 prev.seed=-302528351
BTrace 提供了命令行工具,但使用起不如在 JVisualVM 中方便,下面通過 JVisualVM 中集成 BTrace 插件進行簡單的演示。
JVisualVM 環境中使用 BTrace
安裝 JVisualVM 插件的操作,我們在介紹JVisualVM的時候講過。在安裝 JVisualVM 的插件時,有一款插件叫做“BTrace Workbench”。安裝這款插件之后,在對應的 JVM 實例上點右鍵,就可以進入 BTrace 的操作界面。
打開后默認的界面如下:
可以看到這是一個 Java 文件的樣子。然后我們參考官方文檔,加一些腳本進去。
BTrace 腳本示例
我們下載的 BTrace 項目中,samples 目錄下有一些腳本示例。 參照這些示例,編寫一個簡單的 BTrace 腳本:
import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; @BTrace public class TracingScript { @OnMethod( clazz = "/com.xiaojie.jvm.*/", method = "/.*/" ) // 方法進入時 public static void simple( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod) { print("entered " + probeClass); println("." + probeMethod); } @OnMethod(clazz = "com.xiaojie.jvm.RandomSample", method = "randomHash", location = @Location(Kind.RETURN) ) // 方法返回時 public static void onMethodReturn( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod, @Duration long duration, @Return int returnValue) { print(probeClass + "." + probeMethod); print(Strings.strcat("(), duration=", duration+"ns;")); println(Strings.strcat(" return: ", ""+returnValue)); } }
點擊 start 執行
執行結果
可以看到,輸出了簡單的執行結果:
和示例程序的控制台輸出比對,結果一致。
更多工具
OOM Killer
Linux 系統上的 OOM Killer(Out Of Memory killer,OOM 終結者)。假如物理內存不足,Linux 會找出“一頭比較壯的進程”來殺掉。
OOM Killer 參數調優
Java 的堆內存溢出(OOM),是指堆內存用滿了,GC 沒法回收導致分配不了新的對象。
而操作系統的內存溢出(OOM),則是指計算機所有的內存(物理內存 + 交換空間),都被使用滿了。
這種情況下,默認配置會導致系統報警,並停止正常運行。當然,將 /proc/sys/vm/panic_on_oom 參數設置為 0 之后,則系統內核會在發生內存溢出時,自動調用 OOM Killer 功能,來殺掉最壯實的那頭進程(Rogue Process,流氓進程),這樣系統也許就可以繼續運行了。
以下參數可以基於單個進程進行設置,以手工控制哪些進程可以被 OOM Killer 終結。這些參數位於 proc 文件系統中的 /proc/pid/ 目錄下,其中 pid 是指進程的 ID。
- oomadj:正常范圍是 -16 到 15,用於計算一個進程的 OOM 評分(oomscore)。這個分值越高,該進程越有可能被 OOM Killer 給干掉。如果設置為 -17,則禁止 OOM Killer 殺死該進程。
- proc 文件系統是虛擬文件系統,某個進程被殺掉,則 /proc/pid/ 目錄也就被銷毀了。
OOM Killer 參數調整示例
例如進程的 pid=12884
,root 用戶執行:
$ cat /proc/12884/oom_adj 0 # 查看最終得分 $ cat /proc/12884/oom_score 161 $ cat /proc/12884/oom_score_adj 0 # 修改分值 ... $ echo -17 > /proc/12884/oom_adj $ cat /proc/12884/oom_adj -17 $ cat /proc/12884/oom_score 0 # 查看分值修正值 $ cat /proc/12884/oom_score_adj -1000 # 修改分值 $ echo 15 > /proc/12884/oom_adj $ cat /proc/12884/oom_adj 15 $ cat /proc/12884/oom_score 1160 $ cat /proc/12884/oom_score_adj 1000
這樣配置之后,就允許某個占用了最多資源的進程,在操作系統內存不足時,也不會殺掉他,而是先去殺別的進程。
抽樣分析器(Profilers)
相對於前面的工具,分析器只關心 GC 中的一部分領域,這里只簡單介紹分析器相關的 GC 功能。
需要注意:不要認為分析器適用於所有的場景。分析器有時確實作用很大,比如檢測代碼中的 CPU 熱點時,但某些情況使用分析器不一定是個好方案。
對 GC 調優來說也是一樣的。要檢測是否因為 GC 而引起延遲或吞吐量問題時,不需要使用分析器。前面提到的工具(jstat 或原生/可視化 GC 日志)就能更好更快地檢測出是否存在 GC 問題.。特別是從生產環境中收集性能數據時,最好不要使用分析器,因為性能開銷非常大,對正在運行的生產系統會有影響。
如果確實需要對 GC 進行優化,那么分析器就可以派上用場了,可以對 Object 的創建信息一目了然。換個角度看,如果 GC 暫停的原因不在某個內存池中,那就只會是因為創建對象太多了。所有分析器都能夠跟蹤對象分配(via allocation profiling),根據內存分配的軌跡,讓你知道 實際駐留在內存中的是哪些對象。
分配分析能定位到在哪個地方創建了大量的對象。使用分析器輔助進行 GC 調優的好處是,能確定哪種類型的對象最占用內存,以及哪些線程創建了最多的對象。
下面我們通過實例介紹 3 種分配分析器:hprof、JVisualVM 和 AProf。實際上還有很多分析器可供選擇,有商業產品,也有免費工具,但其功能和應用基本上都是類似的。
hprof
hprof 分析器內置於 JDK 之中。在各種環境下都可以使用,一般優先使用這款工具。
性能分析工具——HPROF 簡介:https://github.com/cncounter/translation/blob/master/tiemao2017/20hprof/20_hprof.md
HPROF 參考文檔:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr008.html
要讓 hprof 和程序一起運行,需要修改啟動腳本,類似這樣:
java -agentlib:hprof=heap=sites com.yourcompany.YourApplication
在程序退出時,會將分配信息 dump(轉儲)到工作目錄下的 java.hprof.txt 文件中。使用文本編輯器打開,並搜索“SITES BEGIN”關鍵字,可以看到:
JDK 還自帶了其他工具,比如 jsadebugd 可以在服務端主機上,開啟 RMI Server。jhat 可用於解析 hprof 內存 Dump 文件等。 在此不進行介紹,有興趣可以搜索看看。