前段時間在給公司項目作性能分析,從簡單的分析Log(GC log, postgrep log, hibernate statitistic),到經過AOP蒐集軟件運行數據,再到PET測試,感受時間花了很多,性能也有必定的提高,但總感受像是工做在原始時代,沒法簡單順暢,又無比清晰的得到想要的結果。遂花費了必定的時間,重新梳理學習了一下以前用過的關於jvm調優和內存分析的各類工具,包括JDK自帶的jps, jstack, jmap, jconsole,以及IBM的HeapAnalyzer等,這些工具雖然提供了很多功能,但其可用度,便捷度,遠沒達到IntelliJ之於Java開發那種地步。在偶然狀況下,在雲棲社區上發現有人推薦Jprofiler,裝上使用版一用,發現果真是神器,特此推薦給你們。先聲明,這個軟件是商用的,網上有不少關於lisence的帖子,我這裏轉發,可是毫不推薦你們用破解版!
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
L-Larry_Lau@163.com#7009-14frku31ynzpfr#20176
L-Larry_Lau@163.com#49604-1jfe58we9gyb6#5814
L-Larry_Lau@163.com#25531-1qcev4yintqkj#23927
L-Larry_Lau@163.com#96496-1qsu1lb1jz7g8w#23479
L-Larry_Lau@163.com#20948-11amlvg181cw0p#171159
而後,先轉一篇雲棲上的文章,而後再慢慢開始咱們的Jprofiler之旅。
一.JProfiler是什麼
JProfiler是由ej-technologies GmbH公司開發的一款性能瓶頸分析工具(該公司還開發部署工具)。
其特色:
- 使用方便
- 界面操做友好
- 對被分析的應用影響小
- CPU,Thread,Memory分析功能尤爲強大
- 支持對jdbc,noSql, jsp, servlet, socket等進行分析
- 支持多種模式(離線,在線)的分析
- 跨平臺 (圖1)
二.數據採集
Q1. JProfiler既然是一款性能瓶頸分析工具,這些分析的相關數據來自於哪裏?
Q2. JProfiler是怎麼將這些數據收集並展示的?
A1. 分析的數據主要來自於下面倆部分
1. 一部分來自於jvm的分析接口**JVMTI**(JVM Tool Interface) , JDK必須>=1.6
JVMTI is an event-based system. The profiling agent library can register handler functions for different events. It can then enable or disable selected events
例如: 對象的生命週期,thread的生命週期等信息
2. 一部分來自於instruments classes(可理解爲class的重寫,增長JProfiler相關統計功能)
例如:方法執行時間,次數,方法棧等信息
A2. 數據收集的原理如圖2
1. 用戶在JProfiler GUI中下達監控的指令(通常就是點擊某個按鈕)
2. JProfiler GUI JVM 經過socket(默認端口8849),發送指令給被分析的jvm中的JProfile Agent。
3. JProfiler Agent(若是不清楚Agent請看文章第三部分"啓動模式") 收到指令後,將該指令轉換成相關須要監聽的事件或者指令,來註冊到JVMTI上或者直接讓JVMTI去執行某功能(例如dump jvm內存)
4. JVMTI 根據註冊的事件,來收集當前jvm的相關信息。 例如: 線程的生命週期; jvm的生命週期;classes的生命週期;對象實例的生命週期;堆內存的實時信息等等
5. JProfiler Agent將採集好的信息保存到**內存**中,按照必定規則統計好(若是發送全部數據JProfiler GUI,會對被分析的應用網絡產生比較大的影響)
6. 返回給JProfiler GUI Socket.
7. JProfiler GUI Socket 將收到的信息返回 JProfiler GUI Render
8. JProfiler GUI Render 渲染成最終的展現效果
三. 數據採集方式和啓動模式
A1. JProfier採集方式分爲兩種:Sampling(樣本採集)和Instrumentation
-
Sampling: 相似於樣本統計, 每隔必定時間(5ms)將每一個線程棧中方法棧中的信息統計出來。優勢是對應用影響小(即便你不配置任何Filter, Filter可參考文章第四部分),缺點是一些數據/特性不能提供(例如:方法的調用次數)
-
Instrumentation: 在class加載以前,JProfier把相關功能代碼寫入到須要分析的class中,對正在運行的jvm有必定影響。優勢: 功能強大,但若是須要分析的class多,那麼對應用影響較大,通常配合Filter一塊兒使用。因此通常JRE class和framework的class是在Filter中一般會過濾掉。
注: JProfiler自己沒有指出數據的採集類型,這裏的採集類型是針對方法調用的採集類型 。由於JProfiler的絕大多數核心功能都依賴方法調用採集的數據, 因此能夠直接認爲是JProfiler的數據採集類型。
A2: 啓動模式:
-
Attach mode
可直接將本機正在運行的jvm加載JProfiler Agent. 優勢是很方便,缺點是一些特性不能支持。若是選擇Instrumentation數據採集方式,那麼須要花一些額外時間來重寫須要分析的class。 -
Profile at startup
在被分析的jvm啓動時,將指定的JProfiler Agent手動加載到該jvm。JProfiler GUI 將收集信息類型和策略等配置信息經過socket發送給JProfiler Agent,收到這些信息後該jvm纔會啓動。
在被分析的jvm 的啓動參數增長下面內容:
語法: -agentpath:[path to jprofilerti library]
【注】: 語法不清楚不要緊,JProfiler提供了幫助嚮導.
(圖3) -
Prepare for profiling:
和Profile at startup的主要區別:被分析的jvm不須要收到JProfiler GUI 的相關配置信息就能夠啓動。 -
Offline profiling
通常用於適用於不能直接調試線上的場景。Offline profiling須要將信息採集內容和策略(一些Trigger, Trigger請參考文章第五部分)打包成一個配置文件(config.xml),在線上啓動該jvm 加載 JProfiler Agent時,加載該xml。那麼JProfiler Agent會根據Trigger的類型會生成不一樣的信息。例如: heap dump; thread dump; method call record等
語法:
-agentpath:/home/2080/jprofiler8/bin/Linux-x64/libjprofilerti.so=offline,id=151,config=/home/2080/config.xml
【注】: config.xml中的每個被分析的jvm的採集信息都有一個id來標識。
下面是使用了離線模式,並使用了每隔一秒dump heap 的Trigger:
四. JProfiler核心概念
-
Profiling Settings: 收據收集的策略:Sampling和 Instrumentation,一些數據採集細節能夠自定義.
(圖5) -
Triggers: 通常用於**offline**模式,告知JProfiler Agent 何時觸發什麼行爲來收集指定信息.
(圖6) -
Live memory: class/class instance的相關信息。 例如對象的個數,大小,對象建立的方法執行棧,對象建立的熱點。
(圖7) -
Heap walker: 對必定時間內收集的內存對像信息進行靜態分析,功能強大且使用。包含對象的outgoing reference, incoming reference, biggest object等
(圖8) -
CPU views: CPU消耗的分佈及時間(cpu時間或者運行時間); 方法的執行圖; 方法的執行統計(最大,最小,平均運行時間等)
(圖9) -
Telemetries: 包含heap, thread, gc, class等的趨勢圖(遙測視圖)
五. 實踐
爲了方便實踐,直接以JProfiler8自帶的一個例子來幫助理解上面的相關概念。
JProfiler 自帶的例子以下:模擬了內存泄露和線程阻塞的場景:
具體源碼參考: /jprofiler install path/demo/bezier
(圖13 Leak Memory 模擬內存泄露, Simulate blocking 模擬線程間鎖的阻塞)
A1. 首先來分析下內存泄露的場景:(勾選圖13中 Leak Memory 模擬內存泄露)
1. 在**Telemetries-> Memory**視圖中你會看到大體以下圖的場景(在看的過程當中能夠間隔一段時間去執行Run GC這個功能):看到下圖藍色區域,老生代在gc後(**波谷**)內存的大小在慢慢的增長(理想狀況下,這個值應該是穩定的)
(圖14)
-
在 Live memory->Recorded Objects 中點擊**record allocation data**按鈕,開始統計一段時間內建立的對象信息。執行一次**Run GC**後看看當前對象信息的大小,並點擊工具欄中**Mark Current**按鈕(其實就是給當前對象數量打個標記。執行一次Run GC,而後再繼續觀察;執行一次Run GC,而後再繼續觀察...。最後看看哪些對象在不斷GC後,數量還一直上漲的。最後你看到的信息可能和下圖相似
(圖15 綠色是標記前的數量,紅色是標記後的增量) -
點擊上圖中實例最多的class,右鍵**Use Selected Instances->Reference->Incoming Reference**.
發現該Long數據最終是存放在**bezier.BeaierAnim.leakMap**中。
(圖18)
在Allocations tab項中,右鍵點擊其中的某個方法,可查看到具體的源碼信息.
(圖19)
【注】:到這裏問題已經很是清楚了,明白了在圖17中爲何哪些實例的數量是同樣多,而且爲何內存在fullgc後仍是回收不了(一個old 區的對象leakMap,put的信息也會進入old區, leakMap如回收不掉,那麼該map中包含的對象也回收不掉)。
A2. 模擬線程阻塞的場景(勾選圖13中Simulate blocking 模擬線程間鎖的阻塞)
爲了方便區分線程,我將Demo中的BezierAnim.java的L236的線程命名爲test
public void start() { thread = new Thread(this, "test"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); }
勾選了Demo中"Simulate blocking"選項後,以下圖(注意看下下圖中的狀態圖標), test線程block狀態明顯增長了。
(圖21)
在**Monitors & locks->Monitor History**觀察了一段時間後,會發現有4種發生鎖的狀況。
第一種:
AWT-EventQueue-0 線程持有一個Object的鎖,而且處於Waiting狀態。
圖下方的代碼提示出Demo.block方法調用了object.wait方法。這個仍是比較容易理解的。
(圖22)
第二種:
AWT-EventQueue-0佔有了bezier.BezierAnim$Demo實例上的鎖,而test線程等待該線程釋放。
注意下圖中下方的源代碼, 這種鎖的出現緣由是Demo的blcok方法在AWT和test線程
都會被執行,而且該方法是synchronized.
(圖23)
第三種和第四種:
test線程中會不斷向事件Event Dispatching Thread提交任務,致使競爭java.awt.EventQueue對象鎖。
提交任務的方式是下面的代碼:repaint()
和EventQueue.invokeLater
public void run() { Thread me = Thread.currentThread(); while (thread == me) { repaint(); if (block) { block(false); } try { Thread.sleep(10); } catch (Exception e) { break; } EventQueue.invokeLater(new Runnable() { @Override public void run() { onEDTMethod(); } }); } thread = null; }
六. 最佳實踐
- JProfiler都會對一些特殊操做給予提示,這時候最好仔細閱讀下說明.
- "Mark Current"功能在某些場景頗有效
- Heap walker通常是靜態分析在Live memory->Recorder objects的對象信息,這些信息可能會被GC回收掉,致使Heap walker中什麼也沒有顯示出來。這種現象是正常的。
- 能夠才工具欄中Start Recordings配置一次性收集的信息
- Filter中include和exclude是有順序的,注意使用下圖**左下方**的**「Show Filter Tree」**來驗證一下順序 (圖25)