轉載:https://blog.csdn.net/lmb55/article/details/79267277
一、概述
開發大型 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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
填寫主機名:
右鍵創建一個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; };
- 1
- 2
- 3
然后使用這個策略文件啟動jstatd服務
[sys@sys bin]$ pwd
/home/sys/jdk1.5.0_15/bin [sys@123 sys]$ ./jstatd -J-Djava.security.policy=./jstatd.all.policy &
- 1
- 2
- 3
因為監控的過程中需要jstatd服務一直運行,所以加上了&,如果需要日志也可使用:
./jstatd -J-Djava.security.policy=./jstatd.all.policy -J-Djava.rmi.server.logCalls=true
- 1
接下來就可以在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
- 1
- 2
- 3
- 4
修改完重啟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"); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
配置的JVM參數如下:
-Xms512m -Xmx512m -XX:-UseGCOverheadLimit -XX:MaxPermSize=50m
- 1
- 2
- 3
- 4
使用jVisualvm分析內存泄漏:
查看Visual GC標簽,內容如下,這是輸出first的截圖
這是輸出forth的截圖:
通過2張圖對比發現:
老生代一直在gc,當程序繼續運行可以發現老生代gc還在繼續:
增加到了7次,但是老生代的內存並沒有減少。說明存在無法被回收的對象,可能是內存泄漏了。
如何分析是那個對象泄漏了呢?打開抽樣器標簽:點擊后如下圖:
按照程序輸出進行堆dump,當輸出second時,dump一次,當輸出forth時dump一次。
進入最后dump出來的堆標簽,點擊類:
點擊右上角:“與另一個堆存儲對比”。如圖選擇第一次導出的dump內容比較:
比較結果如下:
可以看出在兩次間隔時間內TestMemory對象實例一直在增加並且多了,說明該對象引用的方法可能存在內存泄漏。
如何查看對象引用關系呢?
右鍵選擇類TestMemory,選擇“在實例視圖中顯示”,如下所示:
左側是創建的實例總數,右側上部為該實例的結構,下面為引用說明,從圖中可以看出在類CyclicDependencies里面被引用了,並且被HashMap引用。如此可以確定泄漏的位置,進而根據實際情況進行分析解決。