背景
性能診斷是軟體工程師在日常工作中需要經常面對和解決的問題,在使用者體驗至上的今天,解決好應用的性能問題能帶來非常大的收益。Java 作為最流行的程式設計語言之一,其應用性能診斷一直受到業界廣泛關注。可能造成 Java 應用出現性能問題的因素非常多,例如線程控制、磁盤讀寫、資料庫通路、網絡I/O、垃圾收集等。想要定位這些問題,一款優秀的性能診斷工具必不可少。本文将介紹 Java 性能診斷過程中的常用工具,并重點介紹其中的優秀代表
JProfiler的基本原理和最佳實踐(本文所作的調研基于
jprofiler10.1.4
)。
Java 性能診斷工具簡介
在 Java 的世界裡,有許多診斷工具可供選擇,既包括像 jmap、jstat 這樣的簡單指令行工具,又包括 JVisualvm、JProfiler 等圖形化綜合診斷工具,同時還有 SkyWalking、ARMS 這樣的針對分布式應用的性能監控系統。下面分别對其進行介紹。
簡單指令行工具
JDK 内置了許多指令行工具,它們可用來擷取目标 JVM 不同方面、不同層次的資訊。
- jinfo - 用于實時檢視和調整目标 JVM 的各項參數。
- jstack - 用于擷取目标 Java 程序内的線程堆棧資訊,可用來檢測死鎖、定位死循環等。
- jmap - 用于擷取目标 Java 程序的記憶體相關資訊,包括 Java 堆各區域的使用情況、堆中對象的統計資訊、類加載資訊等。
- jstat - 一款輕量級多功能監控工具,可用于擷取目标 Java 程序的類加載、JIT 編譯、垃圾收集、記憶體使用等資訊。
- jcmd - 相比 jstat 功能更為全面的工具,可用于擷取目标 Java 程序的性能統計、JFR、記憶體使用、垃圾收集、線程堆棧、JVM 運作時間等資訊。
圖形化綜合診斷工具
使用上述指令行工具或組合能幫您擷取目标 Java 應用性能相關的基礎資訊,但它們存在下列局限:
- 無法擷取方法級别的分析資料,如方法間的調用關系、各方法的調用次數和調用時間等(這對定位應用性能瓶頸至關重要)。
- 要求使用者登入到目标 Java 應用所在的主控端上,使用起來不是很友善。
- 分析資料通過終端輸出,結果展示不夠直覺。
下面介紹幾款圖形化的綜合性能診斷工具。
JVisualvm
是 JDK 内置的可視化性能診斷工具,它通過 JMX、jstatd、Attach API 等方式擷取目标 JVM 的分析資料,包括 CPU 使用率、記憶體使用量、線程堆棧資訊等。此外,它還能直覺地展示 Java 堆中各對象的數量和大小、各 Java 方法的調用次數和執行時間等。
是由 ej-technologies 公司開發的一款 Java 應用性能診斷工具。它聚焦于四個重要主題上。
- 方法調用 - 對方法調用的分析可以幫助您了解應用程式正在做什麼,并找到提高其性能的方法。
- 記憶體配置設定 - 通過分析堆上對象、引用鍊和垃圾收集能幫您修複記憶體洩漏問題,優化記憶體使用。
- 線程和鎖 - JProfiler 提供多種針對線程和鎖的分析視圖助您發現多線程問題。
- 進階子系統 - 許多性能問題都發生在更高的語義級别上。例如,對于JDBC調用,您可能希望找出執行最慢的 SQL 語句。JProfiler 支援對這些子系統進行內建分析。
分布式應用性能診斷
如果隻需要診斷單機 Java 應用的性能瓶頸,上面介紹的診斷工具就已經夠用了。但随着現代系統架構逐漸從單體轉變為分布式、微服務,單純使用上述工具往往無法滿足需求,這時就需要借助
Jaeger、
ARMS SkyWalking這些分布式追蹤系統提供的全鍊路追蹤功能。分布式追蹤系統種類繁多,但實作原理都大同小異,它們通過代碼埋點的方式記錄 tracing 資訊,通過 SDK 或 agent 将記錄的資料傳輸至中央處理系統,最後提供 query 接口對結果進行展示和分析,想了解更多分布式追蹤系統的原理可參考文章
開放分布式追蹤(OpenTracing)入門與 Jaeger 實作。
JProfiler 簡介
核心元件
JProfiler 包含用于采集目标 JVM 分析資料的 JProfiler agent、用于可視化分析資料的 JProfiler UI、提供各種功能的指令行工具,它們之間的關系如下圖所示。

JProfiler agent
JProfiler agent 是一個本地庫,它可以在 JVM 啟動時通過參數
-agentpath:<path to native library>
進行加載或者在程式運作時通過
JVM Attach 機制進行加載。Agent 被成功加載後,會設定 JVMTI 環境,監聽虛拟機産生的事件,如類加載、線程建立等。例如,當它監聽到類加載事件後,會給這些類注入用于執行度量操作的位元組碼。
JProfiler UI
JProfiler UI 是一個可獨立部署的元件,它通過 socket 和 agent 建立連接配接。這意味着不論目标 JVM 運作在本地還是遠端,JProfiler UI 和 agent 間的通信機制都是一樣的。
JProfiler UI 的主要功能是展示通過 agent 采集上來的分析資料,此外還可以通過它控制 agent 的采集行為,将快照儲存至磁盤,展示儲存的快照。
指令行工具
JProfiler 提供了一系列指令行工具以實作不同的功能。
- jpcontroller - 用于控制 agent 的采集行為。它通過 agent 注冊的 JProfiler MBean 向 agent 傳遞指令。
- jpenable - 用于将 agent 加載到一個正在運作的 JVM 上。
- jpdump - 用于擷取正在運作的 JVM 的堆快照。
- jpexport & jpcompare - 用于從儲存的快照中提取資料并建立 HTML 報告。
安裝配置
JProfiler 同時支援診斷本地和遠端 Java 應用的性能。如果您需要實時采集并展示遠端 JVM 的分析資料,需要完成以步驟:
- 在本地安裝 JProfiler UI。
- 在遠端主控端上安裝 JProfiler agent 并讓其被目标 JVM 加載。
- 配置 UI 到 agent 的連接配接。
具體步驟可參考文檔
Installing JProfiler和
Profiling A JVM最佳實踐
本章将以高性能寫 LogHub 類庫
Aliyun LOG Java Producer為原型,帶您了解如何使用 JProfiler 剖析它的性能。如果您的應用或者您在使用 producer 的過程中遇到了性能問題,也可以用類似的方式定位問題根因。如果您還不了解 producer 的功能,建議先閱讀文章
日志上雲利器 - Aliyun LOG Java Producer。本章使用的樣例代碼參見
SamplePerformance.javaJProfiler 設定
資料采集模式
JProfier 提供兩種資料采集模式 Sampling 和 Instrumentation。
- Sampling - 适合于不要求資料完全精确的場景。優點是對系統性能的影響較小,缺點是某些特性不支援(如方法級别的統計資訊)。
- Instrumentation - 完整功能模式,統計資訊也是精确的。缺點是如果需要分析的類比較多,對應用性能影響較大。為了降低影響,往往需要和 Filter 一起使用。
由于我們需要擷取方法級别的統計資訊,這裡選擇了 Instrumentation 模式。同時配置了 Filter,讓 agent 隻記錄位于 Java 包
com.aliyun.openservices.aliyun.log.producer
下的類和類
com.aliyun.openservices.log.Client
的 CPU 分析資料。
應用啟動模式
通過為 JProfiler agent 指定不同的參數可以控制應用的啟動模式。
- 等待模式 - 隻有在 Jprofiler GUI 和 agent 建立連接配接并完成分析配置設定後,應用才會真正啟動。在這種模式下,您能夠擷取應用啟動時期的分析資料。對應的指令為
-agentpath:<path to native library>=port=8849
- 立即啟動模式 - 應用會立即啟動,Jprofiler GUI 會在需要時和 agent 建立連接配接并設定分析配置。這種模式相對靈活,但會丢失應用啟動初期的分析資料。對應的指令為
-agentpath:<path to native library>=port=8849,nowait
- 離線模式 - 通過觸發器記錄資料、儲存快照供事後分析。對應的指令為
-agentpath:<path to native library>=offline,id=xxx,config=/config.xml
因為是在測試環境,同時對應用啟動初期的性能也比較關注,這裡選擇了預設的等待模式。
使用 JProfiler 診斷性能
在完成 JProfiler 的設定後,便可以對 Producer 的性能進行診斷。
Overview
在概覽頁我們可以清晰的看到記憶體使用量、垃圾收集活動、類加載數量、線程個數和狀态、CPU 使用率等名額随時間變化的趨勢。
通過此圖,我們可以作出如下基本判斷:
- 程式在運作過程中會産生大量對象,但這些對象生命周期極短,大部分都能被垃圾收集器及時回收,不會造成記憶體無限增長。
- 加載類的數量在程式初始時增長較快,随後保持平穩,符合預期。
- 在程式運作過程中,有大量線程處于阻塞狀态,需要重點關注。
- 在程式剛啟動時,CPU 使用率較高,需要進一步探究其原因。
CPU views
CPU views 下的各個子視圖展示了應用中各方法的執行次數、執行時間、調用關系等資訊,能幫我們定位對應用性能影響最大的方法。
Call Tree
Call tree 通過樹形圖清晰地展現了方法間的層次調用關系。同時,JProfiler 将子方法按照它們的執行總時間由大到小排序,這能讓您快速定位關鍵方法。
對于 Producer 而言,方法
SendProducerBatchTask.run()
耗時最多,繼續向下檢視會發現該方法的主要時間消耗在了執行方法
Client.PutLogs()
上。
Hot Spots
如果您的應用方法很多,且很多子方法的執行時間比較接近,使用 hot spots 視圖往往能助您更快地定位問題。該視圖能根據方法的單獨執行時間、總執行時間、平均執行時間、調用次數等屬性對它們排序。其中,單獨執行時間等于該方法的總執行時間減去所有子方法的總執行時間。
在該視圖下,可以看到
Client.PutLogs()
,
LogGroup.toByteArray()
SamplePerformance$1.run()
是單獨執行時間耗時最多的三個方法。
Call Graph
找到了關鍵方法後,call graph 視圖能為您呈現與該方法直接關聯的所有方法。這有助于我們對症下藥,制定合适的性能優化政策。
這裡,我們觀察到方法
Client.PutLogs()
執行的主要時間花費在了對象序列化上,是以性能優化的關鍵是提供執行效率更高的序列化方法。
Live memory
Live memory 下的各個子視圖能讓您掌握記憶體的具體配置設定和使用情況,助您判斷是否存在記憶體洩漏問題。
All Objects
All Objects 視圖展示了目前堆中各種對象的數量和總大小。由圖可知,程式在運作過程中構造出了大量 LogContent 對象。
Allocation Call Tree
Allocation Call Tree 以樹形圖的形式展示了各方法配置設定的記憶體大小。可以看到,
SamplePerformance$1.run()
SendProducerBatchTask.run()
是記憶體配置設定大戶。
Allocation Hot Spots
如果方法比較多,您還可以通過 Allocation Hot Spots 視圖快速找出配置設定對象最多的方法。
Thread History
線程曆史記錄視圖直覺地展示了各線程在不同時間點的狀态。
不同線程執行的任務不同,所展現的狀态特征也不同。
- 線程
會循環調用pool-1-thread-<M>
方法異步發送資料,它們在程式剛啟動時一直處于運作狀态,但随後在大部分時間裡處于阻塞狀态。這是因為 producer 發送資料的速率低于資料的産生速率,且單個 producer 執行個體能緩存的資料大小有限。在程式運作初始,producer 有足夠空間緩存待發送資料,是以producer.send()
一直處于運作狀态,這也就解釋了為何程式在剛啟動時 CPU 使用率較高。随着時間的推移,producer 的緩存被逐漸耗盡,pool-1-thread-<M>
必須等到 producer “釋放”出足夠的空間才有機會繼續運作,這也是為什麼我們會觀察到大量線程處于阻塞狀态。pool-1-thread-<M>
-
負責将逾時 batch 投遞到發送線程池中。由于發送速率較快,batch 會因緩存的資料達到了上限被aliyun-log-producer-0-mover
直接投遞到發送線程池中,是以 mover 線程在大部分時間裡都處于等待狀态。pool-1-thread-<M>
-
作為真正執行資料發送任務的線程有一部分時間花在了網絡 I/O 狀态。aliyun-log-producer-0-io-thread-<N>
-
用于處理發送成功的 batch。由于回調函數比較簡單,執行時間短,它在大部分時間裡都處于等待狀态。aliyun-log-producer-0-success-batch-handler
-
用于處理發送失敗的 batch。由于沒有資料發送失敗,它一直處于等待狀态。aliyun-log-producer-0-failure-batch-handler
通過上述分析可知,這些線程的狀态特征都是符合預期的。
Overhead Hot Spots Detected
當程式運作結束後,JProfiler 會彈出一個對話框展示那些頻繁被調用,但執行時間又很短的方法。在下次診斷時,您可以讓 JProfiler agent 在分析過程中忽略掉這些方法以減輕對應用性能的影響。
小結
通過 JProfiler 的診斷可知應用不存在大的性能問題,也不存在記憶體洩漏。下一步的優化方向是提升對象的序列化效率。