一、概述

開發大型 Java 應用程式的過程中難免遇到記憶體洩露、效能瓶頸等問題,比如檔案、網路、資料庫的連線未釋放,未優化的演算法等。隨著應用程式的持續執行,可能會造成整個系統執行效率下降,嚴重的則會造成系統崩潰。為了找出程式中隱藏的這些問題,在專案開發後期往往會使用效能分析工具來對應用程式的效能進行分析和優化。

VisualVM 是一款免費的效能分析工具。它通過 jvmstat、JMX、SA(Serviceability Agent)以及 Attach API 等多種方式從程式執行時獲得實時資料,從而進行動態的效能分析。同時,它能自動選擇更快更輕量級的技術儘量減少效能分析對應用程式造成的影響,提高效能分析的精度。

本文將對 VisualVM 的主要功能逐一介紹並探討如何利用獲得的資料進行效能分析及調優。IBM開發文件

二、背景知識:效能分析的主要方式

1.監視:監視是一種用來檢視應用程式執行時行為的一般方法。通常會有多個檢視(View)分別實時地顯示 CPU 使用情況、記憶體使用情況、執行緒狀態以及其他一些有用的資訊,以便使用者能很快地發現問題的關鍵所在。

2.轉儲:效能分析工具從記憶體中獲得當前狀態資料並存儲到檔案用於靜態的效能分析。Java 程式是通過在啟動 Java 程式時新增適當的條件引數來觸發轉儲操作的。它包括以下三種:

系統轉儲:JVM 生成的本地系統的轉儲,又稱作核心轉儲。一般的,系統轉儲資料量大,需要平臺相關的工具去分析,如 Windows 上的 windbg 和 Linux 上的 gdb.

Java 轉儲:JVM 內部生成的格式化後的資料,包括執行緒資訊,類的載入資訊以及堆的統計資料。通常也用於檢測死鎖。

堆轉儲:JVM 將所有物件的堆內容儲存到檔案。

3.快照:應用程式啟動後,效能分析工具開始收集各種執行時資料,其中一些資料直接顯示在監視檢視中,而另外大部分資料被儲存在內部,直到使用者要求獲取快照,基於這些儲存的資料的統計資訊才被顯示出來。快照包含了應用程式在一段時間內的執行資訊,通常有 CPU 快照和記憶體快照兩種型別。

CPU 快照:主要包含了應用程式中函式的呼叫關係及執行時間,這些資訊通常可以在 CPU 快照檢視中進行檢視。

記憶體快照:主要包含了記憶體的分配和使用情況、載入的所有類、存在的物件資訊及物件間的引用關係等。這些資訊通常可以在記憶體快照檢視中進行檢視。

4.效能分析:效能分析是通過收集程式執行時的執行資料來幫助開發人員定位程式需要被優化的部分,從而提高程式的執行速度或是記憶體使用效率,主要有以下三個方面:

CPU 效能分析:CPU 效能分析的主要目的是統計函式的呼叫情況及執行時間,或者更簡單的情況就是統計應用程式的 CPU 使用情況。通常有 CPU 監視和 CPU 快照兩種方式來顯示 CPU 效能分析結果。

記憶體效能分析:記憶體效能分析的主要目的是通過統計記憶體使用情況檢測可能存在的記憶體洩露問題及確定優化記憶體使用的方向。通常有記憶體監視和記憶體快照兩種方式來顯示記憶體效能分析結果。

執行緒效能分析:執行緒效能分析主要用於在多執行緒應用程式中確定記憶體的問題所在。一般包括執行緒的狀態變化情況,死鎖情況和某個執行緒線上程生命期內狀態的分佈情況等

三、VisualVM 安裝

1、VisualVM 安裝

VisualVM 是一個性能分析工具,自從 JDK 6 Update 7 以後已經作為 Oracle JDK 的一部分,位於 JDK 根目錄的 bin 資料夾下。VisualVM 自身要在 JDK6 以上的版本上執行,但是它能夠監控 JDK1.4 以上版本的應用程式。

VisualVM可以使用JDK自帶的jvisualvm(bin目錄下)也可以單獨下載,經過實驗,發現安裝後的JDK中自帶的jvisualvm包含的外掛比較少大概有五六個左右,單獨下載的外掛包含的比較多大概有24個左右。它也可以監控1.6以前的JDK,但是對某些模組支援的並不是很好,無法顯示。

2、安裝 VisualVM 上的外掛

VisualVM 外掛中心提供很多外掛以供安裝向 VisualVM 新增功能。可以通過 VisualVM 應用程式安裝,或者從 VisualVM 外掛中心手動下載外掛,然後離線安裝。另外,使用者還可以通過下載外掛分發檔案 (.nbm 檔案 ) 安裝第三方外掛為 VisualVM 新增功能。

從 VisualVM 外掛中心安裝外掛安裝步驟 :

1、從主選單中選擇“工具”>“外掛”。
2、在“可用外掛”標籤中,選中該外掛的“安裝”複選框。單擊“安裝”。
3、逐步完成外掛安裝程式

這裡寫圖片描述

根據 .nbm 檔案安裝第三方外掛安裝步驟 :

1、從主選單中選擇“工具”>“外掛”。
2、在“已下載”標籤中,點選”新增外掛”按鈕,選擇已下載的外掛分發檔案 (.nbm) 並開啟。
3、選中開啟的外掛分發檔案,並單擊”安裝”按鈕,逐步完成外掛安裝程式

這裡寫圖片描述

四、功能介紹

下面我們將介紹效能分析的幾種常見方式以及如何使用 VisualVM 效能分析工具進行分析。

1、記憶體分析
VisualVM 通過檢測 JVM 中載入的類和物件資訊等幫助我們分析記憶體使用情況,我們可以通過 VisualVM 的監視標籤和 Profiler 標籤對應用程式進行記憶體分析。
在監視標籤內,我們可以看到實時的應用程式記憶體堆以及永久保留區域的使用情況。
記憶體堆使用情況
這裡寫圖片描述

永久保留區域使用情況
這裡寫圖片描述

此外,我們也可以通過 Applications 視窗右擊應用程式節點來啟用“在出現 OOME 時生成堆 Dump”功能,當應用程式出現 OutOfMemory 例外時,VisualVM 將自動生成一個堆轉儲。
開啟“在出現 OOME 時生成堆”功能
這裡寫圖片描述
在 Profiler 標籤,點選“記憶體”按鈕將啟動一個記憶體分析會話,等 VisualVM 收集和統計完相關效能資料資訊,將會顯示在效能分析結果。通過記憶體效能分析結果,我們可以檢視哪些物件佔用了較多的記憶體,存活的時間比較長等,以便做進一步的優化。
此外,我們可以通過效能分析結果下方的類名過濾器對分析結果進行過濾。
記憶體分析結果
這裡寫圖片描述

2、CPU 分析
VisualVM 能夠監控應用程式在一段時間的 CPU 的使用情況,顯示 CPU 的使用率、方法的執行效率和頻率等相關資料幫助我們發現應用程式的效能瓶頸。我們可以通過 VisualVM 的監視標籤和 Profiler 標籤對應用程式進行 CPU 效能分析。

在監視標籤內,我們可以檢視 CPU 的使用率以及垃圾回收活動對效能的影響。過高的 CPU 使用率可能是由於我們的專案中存在低效的程式碼,可以通過 Profiler 標籤的 CPU 效能分析功能進行詳細的分析。如果垃圾回收活動過於頻繁,佔用了較高的 CPU 資源,可能是由記憶體不足或者是新生代和舊生代分配不合理導致的等。
CPU 使用情況
這裡寫圖片描述
在 Profiler 標籤,點選“CPU”按鈕啟動一個 CPU 效能分析會話 ,VisualVM 會檢測應用程式所有的被呼叫的方法。當進入一個方法時,執行緒會發出一個“method entry”的事件,當退出方法時同樣會發出一個“method exit”的事件,這些事件都包含了時間戳。然後 VisualVM 會把每個被呼叫方法的總的執行時間和呼叫的次數按照執行時長展示出來。

此外,我們也可以通過效能分析結果下方的方法名過濾器對分析結果進行過濾。
CPU 效能分析結果
這裡寫圖片描述

3、執行緒分析
Java 語言能夠很好的實現多執行緒應用程式。當我們對一個多執行緒應用程式進行除錯或者開發後期做效能調優的時候,往往需要了解當前程式中所有執行緒的執行狀態,是否有死鎖、熱鎖等情況的發生,從而分析系統可能存在的問題。

在 VisualVM 的監視標籤內,我們可以檢視當前應用程式中所有活動執行緒和守護執行緒的數量等實時資訊。
活躍執行緒情況
這裡寫圖片描述
VisualVM 的執行緒標籤提供了三種檢視,預設會以時間線的方式展現。另外兩種檢視分別是表檢視和詳細資訊檢視。

時間線檢視上方的工具欄提供了縮小,放大和自適應三個按鈕,以及一個下拉框,我們可以選擇將所有執行緒、活動執行緒或者完成的執行緒顯示在檢視中。
執行緒時間線檢視
這裡寫圖片描述

執行緒表檢視
這裡寫圖片描述
我們在詳細資訊檢視中不但可以檢視所有執行緒、活動執行緒和結束的執行緒的詳細資料,而且也可以檢視某個執行緒的詳細情況。
執行緒詳細檢視
這裡寫圖片描述

五、快照功能

我們可以使用 VisualVM 的快照功能生成任意個性能分析快照並儲存到本地來輔助我們進行效能分析。快照為捕獲應用程式效能分析資料提供了一個很便捷的方式因為快照一旦生成可以在任何時候離線開啟和檢視,也可以相互傳閱。

VisualVM 提供了兩種型別的快照:
1、Profiler 快照:當有一個性能分析會話(記憶體或者 CPU)正在進行時,我們可以通過效能分析結果工具欄的“快照”按鈕生成 Profiler 快照捕獲當時的效能分析資料。
Profiler 快照
這裡寫圖片描述

2、應用程式快照:我們可以右鍵點選左側 Applications 視窗中應用程式節點,選擇“應用程式快照”為生成一個應用程式快照。應用程式快照會收集某一時刻的堆轉儲,執行緒轉儲和 Profiler 快照,同時也會捕獲 JVM 的一些基本資訊。
應用程式快照
這裡寫圖片描述

六、轉儲功能

1、執行緒轉儲的生成與分析

VisualVM 能夠對正在執行的本地應用程式生成執行緒轉儲,把活動執行緒的堆疊蹤跡打印出來,幫助我們有效瞭解執行緒執行的情況,診斷死鎖、應用程式癱瘓等問題。
執行緒標籤及執行緒轉儲功能
這裡寫圖片描述

當 VisualVM 統計完應用程式內執行緒的相關資料,會把這些資訊顯示新的執行緒轉儲標籤。
執行緒轉儲結果
這裡寫圖片描述

2、堆轉儲的生成與分析

VisualVM 能夠生成堆轉儲,統計某一特定時刻 JVM 中的物件資訊,幫助我們分析物件的引用關係、是否有記憶體洩漏情況的發生等。
監視標籤及堆轉儲功能
這裡寫圖片描述

當 VisualVM 統計完堆內物件資料後,會把堆轉儲資訊顯示在新的堆轉儲標籤內,我們可以看到摘要、類、例項數等資訊以及通過 OQL 控制檯執行查詢語句功能。
堆轉儲的摘要包括轉儲的檔案大小、路徑等基本資訊,執行的系統環境資訊,也可以顯示所有的執行緒資訊。
堆轉儲的摘要檢視
這裡寫圖片描述

從類檢視可以獲得各個類的例項數和佔用堆大小數,分析出記憶體空間的使用情況,找出記憶體的瓶頸,避免記憶體的過度使用。
堆轉儲的類檢視
這裡寫圖片描述

通過例項數檢視可以獲得每個例項內部各成員變數的值以及該例項被引用的位置。首先需要在類檢視選擇需要檢視例項的類。
選擇查詢例項數的類
這裡寫圖片描述
例項數檢視
這裡寫圖片描述

此外,還能對兩個堆轉儲檔案進行比較。通過比較我們能夠分析出兩個時間點哪些物件被大量建立或銷燬。
堆轉儲的比較
這裡寫圖片描述
堆轉儲的比較結果
這裡寫圖片描述
執行緒轉儲和堆轉儲均可以另存成檔案,以便進行離線分析。
轉儲檔案的匯出
這裡寫圖片描述

七、實戰分析

1、簡要說明

開啟jdk自帶的jvisualvm(bin目錄下),程式執行後會自動監控本機執行的java程式(Local標籤下,遠端伺服器上的java程式需要另行配置),Local標籤下的第一個VisualVM為jvisualvm對自身的監控,可以看到消耗的資源還是很少的,第二個為本機的eclipse。
這裡寫圖片描述

監控項總共分為Overview,Monitor,Threads和一個Sampler。
(1)Overview(jvm啟動引數,系統引數)
這裡寫圖片描述
可以看到eclipse的啟動引數
這裡寫圖片描述
(通過這些啟動引數,可以判斷程式是否有記憶體溢位)

(2)Monitor
這裡寫圖片描述
左上:cpu利用率,gc狀態的監控
右上:堆利用率,永久記憶體區的利用率
左下:類的監控
右下:執行緒的監控
performGC:gc的詳細執行狀態
HeapDump:堆的詳細狀態(可以看到堆的概況,裡面所有的類,還能點進具體的一個類檢視這個類的狀態)

(3)Threads
這裡寫圖片描述
能夠顯示執行緒的名稱和執行的狀態,在除錯多執行緒時必不可少,而且可以點進一個執行緒檢視這個執行緒的詳細執行情況

2、監控伺服器上的tomcat

tomcat的配置檔案catalina.sh中增加:

JAVA_OPTS="-Dcom.sun.management.jmxremote.port=9998   
    -Dcom.sun.management.jmxremote.ssl=false   
    -Dcom.sun.management.jmxremote.authenticate=false   
    -Djava.rmi.server.hostname=192.168.58.164" 

引數說明:
指定了JMX啟動的代理埠,這個埠就是visualvm要連線的埠(9998埠不能被別的程式使用netstat -an|gerp 9998)  
Dcom.sun.management.jmxremote.port=9998 

指定了JMX是否啟用ssl  
Dcom.sun.management.jmxremote.authenticate=false  

指定了JMX是否啟用鑑權(需要使用者名稱,密碼鑑權)  
Dcom.sun.management.jmxremote.authenticate=false  

指定了伺服器主機名  
Djava.rmi.server.hostname=192.168.58.164  

這裡寫圖片描述
填寫主機名:
這裡寫圖片描述
右鍵建立一個jmx連線,填寫上ip:port即可:
這裡寫圖片描述

3、監控伺服器上的java程式

相較於監控tomcat要麻煩很多,要預先啟動jstatd服務(${java_home}/bin目錄下)
jstatd是一個監控JVM從建立到銷燬過程中資源佔用情況並提供遠端監控介面的RMI(Remote Method Invocation,遠端方法呼叫)伺服器程式,它是一個Daemon程式(後臺程序),要保證遠端監控軟體連線到本地的話需要jstatd始終保持執行。

jstatd執行需要通過-J-Djava.security.policy=*指定安全策略,因此我們需要在伺服器上建立一個指定安全策略的檔案jstatd.all.policy(我放在了${java_home}/bin目錄下),檔案內容如下:

grant codebase "file:/home/123/123/jdk1.5.0_15/lib/tools.jar" {   
    permission java.security.AllPermission;   
};   

然後使用這個策略檔案啟動jstatd服務

[[email protected] bin]$ pwd  
/home/sys/jdk1.5.0_15/bin  
[sys@123 sys]$ ./jstatd -J-Djava.security.policy=./jstatd.all.policy & 

因為監控的過程中需要jstatd服務一直執行,所以加上了&,如果需要日誌也可使用:

./jstatd -J-Djava.security.policy=./jstatd.all.policy -J-Djava.rmi.server.logCalls=true  

接下來就可以在jvisualvm中配置監控該伺服器上執行的java程式了,和在jvisualvm中配置監控tomcat伺服器的操作過程是一樣的。需要特別注意的是,有時在配置遠端監控java程式的時候jvisualvm會報一個錯誤
點選檢視錯誤詳情:
這裡寫圖片描述
connection refused to host:127.0.0.1初步判斷和主機名有關係。

[sys@sys bin]# hostname -i  
127.0.0.1  

[sys@sys bin]# hostname 192.168.58.168 

修改完重啟jstatd服務(網上很多人說要修改主機的/etc/hosts檔案,但是我自己測試修改/etc/hosts檔案是沒有效果的,必須要修改主機名),Add Rempte Host,填寫主機名,之後這裡要選擇新增一個jstatd連線:
這裡寫圖片描述
直接選擇預設配置即可(預設使用1099埠):
這裡寫圖片描述
點選ok後,168上的所有java程式就會自動列出:
這裡寫圖片描述

注意:推薦一個非常好用的外掛VisualGC,tool -> plugin ->aviable plugin:
這裡寫圖片描述
安裝完這個外掛後,將會增加新的監控條目Visual GC,可以看到虛擬機器記憶體各個區的使用情況:
這裡寫圖片描述

4、模擬記憶體洩漏

import java.util.HashMap;  
import java.util.Map;  
public class MemoryLeckTest {  
    //宣告快取物件  
    private static final Map map = new HashMap();  
    public static void main(String args[]){  
        try {  
            Thread.sleep(10000);//給開啟visualvm時間  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        //迴圈新增物件到快取  
        for(int i=0; i<1000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("first");  
        //為dump出堆提供時間  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<1000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("second");  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<3000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("third");  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<4000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("forth");  
        try {  
            Thread.sleep(Integer.MAX_VALUE);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("qqqq");  
    }  
}  

配置的JVM引數如下:

-Xms512m  
-Xmx512m  
 -XX:-UseGCOverheadLimit  
 -XX:MaxPermSize=50m  

使用jVisualvm分析記憶體洩漏:

檢視Visual GC標籤,內容如下,這是輸出first的截圖
這裡寫圖片描述
這是輸出forth的截圖:
這裡寫圖片描述
通過2張圖對比發現:
這裡寫圖片描述
老生代一直在gc,當程式繼續執行可以發現老生代gc還在繼續:
這裡寫圖片描述
增加到了7次,但是老生代的記憶體並沒有減少。說明存在無法被回收的物件,可能是記憶體洩漏了。
如何分析是那個物件洩漏了呢?開啟抽樣器標籤:點選後如下圖:
這裡寫圖片描述
按照程式輸出進行堆dump,當輸出second時,dump一次,當輸出forth時dump一次。
進入最後dump出來的堆標籤,點選類:
這裡寫圖片描述
點選右上角:“與另一個堆儲存對比”。如圖選擇第一次匯出的dump內容比較:
這裡寫圖片描述
比較結果如下:
這裡寫圖片描述
可以看出在兩次間隔時間內TestMemory物件例項一直在增加並且多了,說明該物件引用的方法可能存在記憶體洩漏。
如何檢視物件引用關係呢?
右鍵選擇類TestMemory,選擇“在例項檢視中顯示”,如下所示:
這裡寫圖片描述
左側是建立的例項總數,右側上部為該例項的結構,下面為引用說明,從圖中可以看出在類CyclicDependencies裡面被引用了,並且被HashMap引用。如此可以確定洩漏的位置,進而根據實際情況進行分析解決。

.