
引言
本文為 Java 性能分析工具系列文章第二篇,第一篇:操作系統工具。在本文中將介紹如何使用 Java 內置監控工具更加深入的了解 Java 應用程序和 JVM 本身。在 JDK 中有許多內置的工具,其中包括:
- jcmd:打印一個 Java 進程的類,線程以及虛擬機信息。適合用在腳本中。使用 jcmd - h 來查看使用方法。
- jconsole:提供 JVM 活動的圖形化展示,包括線程使用,類使用以及垃圾回收(GC)信息。
- jhat:幫助分析內存堆存儲。
- jmap:提供 JVM 內存使用信息,適用于腳本中。
- jinfo:訪問 JVM 系統屬性,同時可以動態修改這些屬性。
- jstack:提供 Java 進程內的線程堆棧信息。
- jstat:提供 Java 垃圾回收以及類加載信息。
- jvisualvm:監控 JVM 的可視化工具,剖析運行中的應用程序,分析 JVM 堆存儲。
下面將根據功能劃分來詳細介紹這些工具。
VM 基本信息
JVM 工具能夠提供一個運行中的 JVM 進程的基本信息,例如運行時間、使用中的 JVM 參數以及 JVM 系統屬性。
- uptimeJVM 運行的時間,jcmd process_id VM.uptime
- system properties通過 System.getProperties() 可以得到的系統屬性也可以通過下面的命令獲得:
jcmd process_id VM.system_properties 或者 jinfo –sysprops process_id
這些屬性包括所有通過命令行-D 選項設置的屬性、應用程序動態添加的屬性和 JVM 的默認屬性。
- JVM version通過 jcmd process_id VM.version 獲得。
- JVM command lineJVM 命令行可以在 jconsole 中的 VM summary 中找到,或者通過 jcmd process_id VM.command_line 命令獲得。
- JVM 調優參數通過命令 jcmd process_id VM.flags [-all] 命令或者所有生效的調優參數獲得。
使用調優參數(Tuning Flags)
由于調優參數非常繁多,需要借助 JVM 命令行和 JVM 調優參數來使用。使用 command_line 命令可以獲得命令行中指定的調優參數,flags 命令可以獲得通過命令設置的調優參數和 JVM 設置的調優參數。
通過 jcmd 命令可以獲得一個運行中 JVM 內生效的調優參數。通過下面這條命令可以獲得一個指定平臺內生效的調優參數。
java
我們需要把其他選項同時包含在這條命令中,尤其是設置了 GC 相關的調優參數。這條命令的部分輸出如下所示,第一行中的冒號說明第一行的調優參數使用的不是默認值,而是以下三種方式設置:
- 通過命令行設置其他選項間接改變了此調優參數的值
- JVM 計算出默認值第二行由于沒有包含冒號,說明此行的調優參數為當前 JVM 版本的默認值,最后一列的 product 說明此行的調優參數的值在不同平臺相同,而 pd product 說明此行的調優參數的值依賴于平臺。
uintx
最后一列的其他選項:
manageable
jinfo 命令可以查看某個單一 flag 的值,通過下面的命令:
jinfo
3.通過下面的命令可以設置某個 flag 的 manageable 屬性來控制其能否在運行時被改變:
jinfo
盡管 jinfo 命令可以改變任何 flag 的值,但不能確定 JVM 會接受這些改變。例如很多影響垃圾回收算法執行的 flag 都會在 JVM 啟動時被設置,在 JVM 運行過程中通過 jinfo 命令修改 flag 的值并不會影響算法執行。所有此命令只對那些 manageable 為真的 flag 起作用。
線程信息
jconsole 和 jvisualvm 命令可以幫助開發人員剖析應用程序運行過程中線程的相關信息。通過 jstack process_id 命令可以查看線程的運行時棧信息,可以明確獲得當前線程是否被阻塞。通過命令 jcmd process_id Thread.print 可以獲得相同結果。
類信息
通過 jconsole 和 jstat 命令可以獲得應用程序運行過程中的所有類的相關信息,同時 jstat 命令也提供了類編譯的相關信息。
垃圾回收信息
Jconsole 展示了 JVM 堆使用的情況,它所繪制的的動態圖能夠幫助開發人員了解堆的內部情況。jcmd 支持垃圾回收操作。jmap 提供堆信息總覽。jstat 從不同的角度展示垃圾回收是如何工作的。
Heap Dump 文件的后序處理
通過 jvisualvm 用戶界面可以得到 Heap Dump 文件,通過 jcmd 和 jmap 也可以獲得。Heap Dump 文件是堆的快照,一般使用 jvisualvm 和 jhat 來分析這個快照。
性能分析工具
Java 提供的性能分析器是最重要的分析工具。它的種類繁多,各有所長,使用不同的分析器在分析同一個應用時可能會發現不同的問題。在使用過程中需要各取所長,這樣才能對應用進行全面的分析。
基本上所有的 Java 性能分析器都是用 Java 實現的,通過套接字(socket)與被分析應用進行通信來獲得被分析應用的運行信息。需要注意的是,在使用性能分析工具調優被分析應用的同時,需要關注性能分析器其自身的性能。假如當被分析的應用程序產生十分龐大的信息,而將其發送至性能分析器時,如果性能分析器沒有空間充分管理高效的內存堆來處理這些信息時,分析將無法進行。采用并行垃圾回收算法進行內存管理是當前性能分析器比較流行的做法,這種算法能夠最大程度地降低內存溢出的可能。
性能分析分為采樣模式和檢測模式。下面將分別介紹這兩種模式。
采樣分析
采樣模式是性能分析中最常用的模式,因為其對被分析應用程序影響最小,這一點非常重要。只有當性能分析過程對應用程序的影響降到最低,才能獲得有價值的性能分析結果。
在采樣分析模式中,分析器被定時觸發工作。在工作周期內,分析器依次檢查每個線程并記錄線程中正在運行的方法,在某些特定場景下,采樣分析往往會帶來錯誤的分析結果。例如,在圖 1 中,某線程在一段時間內交替執行方法 A 和方法 B,每次當分析器被觸發工作時,該線程都恰好在執行方法 B,那么分析器會認為該線程的所有時間都是在執行方法 B,但是事實并非如此,該線程執行方法 A 的時間遠大于執行方法 B 的時間,只是并未被分析器采樣到。
圖 1. 某一時間段內線程交替執行方法 A 和 B 示例圖

這是采樣模式中最常見的錯誤,通過增加采樣分析器的采樣時間隔可以幫助我們有效的減少這類錯誤的發生,因為時間間隔太小往往會增加采樣分析器對被分析的應用程序產生性能方面的影響,從而導致分析結果失真。所以時間間隔需要根據被分析應用的特點通過多次的試驗以及經驗來決定,權衡過大或過小的影響之后設定。
圖 2. 采樣模式分析示例圖

圖 2 所示是使用采樣模式分析一個應用服務器 GlassFish 啟動過程的結果。從圖中可以看到,方法 defineClass1() 使用了 19%的時間,接下來是方法 getPackageSourceInternal(),占用了 10%的時間。Java 應用程序中定義的類會影響應用程序啟動過程中的性能表現,為了提高應用程序的啟動速度,就必須通過提高類加載的速度,從而達到提升啟動速度的目標。從圖中我們可能會錯誤的認為要改善性能的方法是 defineClass1(),但是 defineClass1() 其實是 JDK 中的方法,我們不可能通過重寫 JVM 來提高它的性能。即使重寫此方法將其執行時間優化至原有時間的 60%,也只能減少 10%應用程序整體運行時間,這顯然得不償失。
檢測分析
相比于采樣模式,檢測模式是要侵入被分析的應用程序內部,雖然這樣做并不是高效、友好的,但它卻可以獲得非常有價值的信息。圖 3 為使用相同分析工具的檢測模式分析相同應用服務器 GlassFish 的結果。
圖 3. 檢測分析示例圖

在圖中有以下幾點信息:
- 最耗時的方法為 getPackageSourcesInternal(), 占用了 13%的時間,而并非在采樣模式中得到的 4%;
- 方法 defineClaass1() 并未出現在分析結果中。
- 分析結果中包括每個方法執行的次數和平均耗時。
這些分析結果中的信息對于發現耗時多的代碼是非常有幫助的。在本例中,盡管方法 ImmutableMap.get() 消耗 12%的時間,但是它被調用了四百七十萬次之多。如果減少此方法的調用次數,應用的性能將會得到大幅度提升。
檢測分析器在類被加載時通過改變其字節碼順序來獲取應用運行數據,例如增加記錄方法被調用次數的代碼。相比于采樣模式,這種方式會更大程度的影響應用本身的性能。例如,JVM 會根據方法的代碼塊大小,將方法體很小的方法內聯化,這樣在內聯方法執行時就不會進行方法調用。在檢測分析器在內聯方法中加入其代碼后,此方法因為方法體過大并未被 JVM 內聯化,由此造成此方法的耗時被放大。內聯化只是一個例子,當越來越多的代碼被改變的時候,分析的結果失真的概率就會比較大。
造成方法 ImmutableMap.get() 沒有出現在采樣模式分析結果中的原因是安全點(safepoint)的存在。只有當一個線程獲得的內存大于安全點時,采樣分析器才會對其進行分析。因為方法 ImmutableMap.get() 所在線程一直沒有達到安全點,所以在結果中不會出現。當使用采樣模式安全點過高時,會低估一些方法對性能的影響。
在本例中,無論是采樣分析還是檢測分析,都能發現應用的性能瓶頸在于類的加載和解析。但是在實際中,不同的分析器不可能得出完全相同的分析結果。分析器擅長估量,但也只是估量,一些誤差甚至是錯誤不可避免,所以在性能分析過程中還需要我們更加靈活的使用分析器。
阻塞方法和線程的時間軸
如圖 4 所示為使用 NetBeans Profiler(另一種檢測分析器)分析上述應用服務器 GlassFish 啟動過程的結果展示。在此結果中,方法 park(),parkNanos() 和 read() 占用了絕大多數的應用運行時間。這些方法都是被阻塞的方法,并不消耗 CPU,所以在計算應用的 CPU 使用率時這些時間不應計入。應用中的線程并沒有使用 632 秒來執行 parkNanos() 方法,而是等待其他操作完成花費 632 秒。park() 和 read() 方法與此同理。
圖 4. NetBeans 檢測分析示例圖

因此,大多數的分析器都不會將被阻塞的方法和閑置的線程計入結果。在 NetBeans 中,可以設置分析結果包含所有的方法,所以在本例中這些方法被計入結果。在本例中,執行 park() 方法的線程位于服務器線程池中,當服務器接收到請求時,這些線程處理請求。當沒有請求時,這些線程處于阻塞狀態,等到新的請求,并不占用 CPU。這是應用服務器的正常狀態。
絕大多數的基于 Java 的分析器都可以提供過濾器功能來查看或者隱藏被阻塞方法調用的時間,如果需要可以使用該功能。通常情況下,查看線程的運行狀況比查看被阻塞方法的阻塞時間更加有幫助。
圖 5. Oracle Solaris Studio 中線程的運行示例圖

圖 5 為在 Oracle Solaris Studio 中一個線程的運行情況。每一個水平區域代表一個不同的線程,所以上圖中有兩個線程(1.3 和 1.2)。不同顏色的柱子代表執行的不同方法;空白處代表該線程沒有執行任何方法。綜合來看,線程 1.2 先執行了一段代碼然后等待線程 1.3 完成執行,線程 1.3 完成執行后等待線程 1.2 執行另一段代碼。深入下去可以發現這些線程如何進行交互。
圖中存在一些沒有線程執行的空白區域,這是因為圖中只展示了其中的兩個線程,所以在那段空白區域是圖中所示兩個線程在等待其他線程執行完成。
本地分析器
本地分析器是用來分析 JVM 本身的工具。通過本地分析器可以觀察到 JVM 正在進行的操作或者查看是否有應用程序包含了 JVM 的本地庫,也可以觀察到代碼內部。任何本地分析器都可以分析使用 C 語言實現的 JVM(包括所有本地庫),但是一些本地分析其不能分析使用 Java 和 C++實現的應用。
圖 6. 本地分析器分析示例圖

圖 6 中展示了使用 Oracle Solaris Studio 分析器中分析 GlassFish 啟動過程的結果。Oracle Solaris Studio 是一個可以分析 Java 和 C++的本地分析器。從圖中可以發現,應用消耗的 CPU 時間為 25.1 秒。其中 JVM-System 消耗 20 秒,包括 JVM 編譯器線程,垃圾回收線程以及一些輔助線程。由于在啟動過程中需要編譯非常多的代碼,所以 JVM 編譯器線程消耗了絕大多數時間,而垃圾回收線程只消耗了很少的時間。
通過本地分析器我們不僅可以分析優化 JVM 自身功能,更重要的是可以獲得應用程序進行垃圾回收的時間。在 Java 分析工具中,垃圾回收線程的信息是無法得到的。
分析過 JVM 本地代碼后,我們將對應用程序的啟動過程進行分析。如圖 7 所示,繼在采樣模式分析后,方法 defineclass1() 又一次被分析為最耗時的方法。值得關注的是,再次分析結果中,解壓讀取 jar 文件的方法耗時相對較多。類加載中會用到這些方法,所以證明優化的方向是正確的。由于 Java zip 庫中引用的本地代碼在其他分析工具中被作為阻塞方法調用,所以在上文各類工具中并沒有發現此方法。
圖 7. 采樣模式分析示例圖

無論使用何種性能分析工具,最重要的是熟悉每種工具的優勢和劣勢。這樣才能取長補短,配合使用。開發人員必須學會如何使用性能分析器來找到性能瓶頸,找到需要優化的代碼,而不是單純的關注最耗時的個別方法。
總結
基于采樣的性能分析是最常見的一種,因為其相對能做到的分析是有限的,亦或者分析過程所能搜集到的信息是概述性的,往往并不能真實表現應用程序內部的運行情況,但是其分析過程中引入的工作量通常是較低的。不同的采樣分析工具行為是不同的,充分利用其優勢,做有針對性的分析才是最有意義的。
檢測分析能夠獲得非常多的有關應用程序內部信息,但是前期準備工作往往是非常大的。檢測分析方法應當盡量應用在一小節代碼中,或者少數幾個類、包中。這種方法其實一定程度上限制了對整體應用程序的性能分析,僅適合在程序單元中使用,點對點,針對性較強的分析,采用檢測分析的時候,更多時間要求開發人員明確知道哪里有可能產生性能瓶頸。
線程阻塞不一定就是代碼編寫而產生的,發生線程阻塞時,更多的建議是去想,去看為什么會被阻塞,而不是直接查看代碼。盡量采用線程執行時間軸的分析方法。
本地分析提供了既可以深入查看 JVM 內部,同時也可以查看應用程序代碼執行的情況。
如果本地分析顯示在 GC 過程中大量的使用 CPU 資源,那么調優收集器就是必要的。需要提醒大家的是,編譯線程通常是不影響應用程序的性能。
原作者:李 偉軍, 宋 翰瀛, 和 楊 翔宇
原文鏈接:Java 性能分析工具 , 第 2 部分:Java 內置監控工具
原出處:IBM Developer
