本文爲 Java 性能分析工具系列文章第二篇,第一篇:操做系統工具。在本文中將介紹如何使用 Java 內置監控工具更加深刻的瞭解 Java 應用程序和 JVM 自己。在 JDK 中有許多內置的工具,其中包括:html
下面將根據功能劃分來詳細介紹這些工具。java
回頁首算法
JVM 工具可以提供一個運行中的 JVM 進程的基本信息,例如運行時間、使用中的 JVM 參數以及 JVM 系統屬性。安全
JVM 運行的時間,jcmd process_id VM.uptime服務器
經過 System.getProperties() 能夠獲得的系統屬性也能夠經過下面的命令得到:socket
jcmd process_id VM.system_properties 或者 jinfo –sysprops process_id工具
這些屬性包括全部經過命令行-D 選項設置的屬性、應用程序動態添加的屬性和 JVM 的默認屬性。性能
經過 jcmd process_id VM.version 得到。優化
JVM 命令行能夠在 jconsole 中的 VM summary 中找到,或者經過 jcmd process_id VM.command_line 命令得到。ui
經過命令 jcmd process_id VM.flags [-all] 命令或者全部生效的調優參數得到。
因爲調優參數很是繁多,須要藉助 JVM 命令行和 JVM 調優參數來使用。使用 command_line 命令能夠得到命令行中指定的調優參數,flags 命令能夠得到經過命令設置的調優參數和 JVM 設置的調優參數。
經過 jcmd 命令能夠得到一個運行中 JVM 內生效的調優參數。經過下面這條命令能夠得到一個指定平臺內生效的調優參數。
java other_options –XX:+PrintFlagsFinal –version
咱們須要把其餘選項同時包含在這條命令中,尤爲是設置了 GC 相關的調優參數。這條命令的部分輸出以下所示,第一行中的冒號說明第一行的調優參數使用的不是默認值,而是如下三種方式設置:
其餘選項間接改變了此調優參數的值
第二行因爲沒有包含冒號,說明此行的調優參數爲當前 JVM 版本的默認值,最後一列的 product 說明此行的調優參數的值在不一樣平臺相同,而 pd product 說明此行的調優參數的值依賴於平臺。
uintx InitialHeapSize := 4169431040 {product} intx InlineSmallCode = 2000 {pd product}
最後一列的其餘選項:
manageable:此 flag 的值能夠在運行時動態改變 c2 diagnostic:此 flag 提供幫助工程師理解的編譯器如何工做的幫助信息。
jinfo 命令能夠查看某個單一 flag 的值,經過下面的命令:
jinfo -flag PringGCDetails process_id –XX:+PrintGCDetails
jinfo -flag -printgcdetails process_id # 關閉 PrintGCDetails 的 manageable 屬性
儘管 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 從不一樣的角度展現垃圾回收是如何工做的。
經過 jvisualvm 用戶界面能夠獲得 Heap Dump 文件,經過 jcmd 和 jmap 也能夠得到。Heap Dump 文件是堆的快照,通常使用 jvisualvm 和 jhat 來分析這個快照。
Java 提供的性能分析器是最重要的分析工具。它的種類繁多,各有所長,使用不一樣的分析器在分析同一個應用時可能會發現不一樣的問題。在使用過程當中須要各取所長,這樣才能對應用進行全面的分析。
基本上全部的 Java 性能分析器都是用 Java 實現的,經過套接字(socket)與被分析應用進行通訊來得到被分析應用的運行信息。須要注意的是,在使用性能分析工具調優被分析應用的同時,須要關注性能分析器其自身的性能。假如當被分析的應用程序產生十分龐大的信息,而將其發送至性能分析器時,若是性能分析器沒有空間充分管理高效的內存堆來處理這些信息時,分析將沒法進行。採用並行垃圾回收算法進行內存管理是當前性能分析器比較流行的作法,這種算法可以最大程度地下降內存溢出的可能。
性能分析分爲採樣模式和檢測模式。下面將分別介紹這兩種模式。
採樣模式是性能分析中最經常使用的模式,由於其對被分析應用程序影響最小,這一點很是重要。只有當性能分析過程對應用程序的影響降到最低,才能得到有價值的性能分析結果。
在採樣分析模式中,分析器被定時觸發工做。在工做週期內,分析器依次檢查每一個線程並記錄線程中正在運行的方法,在某些特定場景下,採樣分析每每會帶來錯誤的分析結果。例如,在圖 1 中,某線程在一段時間內交替執行方法 A 和方法 B,每次當分析器被觸發工做時,該線程都剛好在執行方法 B,那麼分析器會認爲該線程的全部時間都是在執行方法 B,可是事實並不是如此,該線程執行方法 A 的時間遠大於執行方法 B 的時間,只是並未被分析器採樣到。
這是採樣模式中最多見的錯誤,經過增長採樣分析器的採樣時間隔能夠幫助咱們有效的減小這類錯誤的發生,由於時間間隔過小每每會增長採樣分析器對被分析的應用程序產生性能方面的影響,從而致使分析結果失真。因此時間間隔須要根據被分析應用的特色經過屢次的試驗以及經驗來決定,權衡過大或太小的影響以後設定。
圖 2 所示是使用採樣模式分析一個應用服務器 GlassFish 啓動過程的結果。從圖中能夠看到,方法 defineClass1() 使用了 19%的時間,接下來是方法 getPackageSourceInternal(),佔用了 10%的時間。Java 應用程序中定義的類會影響應用程序啓動過程當中的性能表現,爲了提升應用程序的啓動速度,就必須經過提升類加載的速度,從而達到提高啓動速度的目標。從圖中咱們可能會錯誤的認爲要改善性能的方法是 defineClass1(),可是 defineClass1() 實際上是 JDK 中的方法,咱們不可能經過重寫 JVM 來提升它的性能。即便重寫此方法將其執行時間優化至原有時間的 60%,也只能減小 10%應用程序總體運行時間,這顯然得不償失。
相比於採樣模式,檢測模式是要侵入被分析的應用程序內部,雖然這樣作並非高效、友好的,但它卻能夠得到很是有價值的信息。圖 3 爲使用相同分析工具的檢測模式分析相同應用服務器 GlassFish 的結果。
在圖中有如下幾點信息:
這些分析結果中的信息對於發現耗時多的代碼是很是有幫助的。在本例中,儘管方法 ImmutableMap.get() 消耗 12%的時間,可是它被調用了四百七十萬次之多。若是減小此方法的調用次數,應用的性能將會獲得大幅度提高。
檢測分析器在類被加載時經過改變其字節碼順序來獲取應用運行數據,例如增長記錄方法被調用次數的代碼。相比於採樣模式,這種方式會更大程度的影響應用自己的性能。例如,JVM 會根據方法的代碼塊大小,將方法體很小的方法內聯化,這樣在內聯方法執行時就不會進行方法調用。在檢測分析器在內聯方法中加入其代碼後,此方法由於方法體過大並未被 JVM 內聯化,由此形成此方法的耗時被放大。內聯化只是一個例子,當愈來愈多的代碼被改變的時候,分析的結果失真的機率就會比較大。
形成方法 ImmutableMap.get() 沒有出如今採樣模式分析結果中的緣由是安全點(safepoint)的存在。只有當一個線程得到的內存大於安全點時,採樣分析器纔會對其進行分析。由於方法 ImmutableMap.get() 所在線程一直沒有達到安全點,因此在結果中不會出現。當使用採樣模式安全點太高時,會低估一些方法對性能的影響。
在本例中,不管是採樣分析仍是檢測分析,都能發現應用的性能瓶頸在於類的加載和解析。可是在實際中,不一樣的分析器不可能得出徹底相同的分析結果。分析器擅長估量,但也只是估量,一些偏差甚至是錯誤不可避免,因此在性能分析過程當中還須要咱們更加靈活的使用分析器。
如圖 4 所示爲使用 NetBeans Profiler(另外一種檢測分析器)分析上述應用服務器 GlassFish 啓動過程的結果展現。在此結果中,方法 park(),parkNanos() 和 read() 佔用了絕大多數的應用運行時間。這些方法都是被阻塞的方法,並不消耗 CPU,因此在計算應用的 CPU 使用率時這些時間不該計入。應用中的線程並無使用 632 秒來執行 parkNanos() 方法,而是等待其餘操做完成花費 632 秒。park() 和 read() 方法與此同理。
所以,大多數的分析器都不會將被阻塞的方法和閒置的線程計入結果。在 NetBeans 中,能夠設置分析結果包含全部的方法,因此在本例中這些方法被計入結果。在本例中,執行 park() 方法的線程位於服務器線程池中,當服務器接收到請求時,這些線程處理請求。當沒有請求時,這些線程處於阻塞狀態,等到新的請求,並不佔用 CPU。這是應用服務器的正常狀態。
絕大多數的基於 Java 的分析器均可以提供過濾器功能來查看或者隱藏被阻塞方法調用的時間,若是須要可使用該功能。一般狀況下,查看線程的運行情況比查看被阻塞方法的阻塞時間更加有幫助。
圖 5 爲在 Oracle Solaris Studio 中一個線程的運行狀況。每個水平區域表明一個不一樣的線程,因此上圖中有兩個線程(1.3 和 1.2)。不一樣顏色的柱子表明執行的不一樣方法;空白處表明該線程沒有執行任何方法。綜合來看,線程 1.2 先執行了一段代碼而後等待線程 1.3 完成執行,線程 1.3 完成執行後等待線程 1.2 執行另外一段代碼。深刻下去能夠發現這些線程如何進行交互。
圖中存在一些沒有線程執行的空白區域,這是由於圖中只展現了其中的兩個線程,因此在那段空白區域是圖中所示兩個線程在等待其餘線程執行完成。
本地分析器是用來分析 JVM 自己的工具。經過本地分析器能夠觀察到 JVM 正在進行的操做或者查看是否有應用程序包含了 JVM 的本地庫,也能夠觀察到代碼內部。任何本地分析器均可以分析使用 C 語言實現的 JVM(包括全部本地庫),可是一些本地分析其不能分析使用 Java 和 C++實現的應用。
圖 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 庫中引用的本地代碼在其餘分析工具中被做爲阻塞方法調用,因此在上文各種工具中並無發現此方法。
不管使用何種性能分析工具,最重要的是熟悉每種工具的優點和劣勢。這樣才能取長補短,配合使用。開發人員必須學會如何使用性能分析器來找到性能瓶頸,找到須要優化的代碼,而不是單純的關注最耗時的個別方法。
基於採樣的性能分析是最多見的一種,由於其相對能作到的分析是有限的,亦或者分析過程所能蒐集到的信息是概述性的,每每並不能真實表現應用程序內部的運行狀況,可是其分析過程當中引入的工做量一般是較低的。不一樣的採樣分析工具行爲是不一樣的,充分利用其優點,作有針對性的分析纔是最有意義的。
檢測分析可以得到很是多的有關應用程序內部信息,可是前期準備工做每每是很是大的。檢測分析方法應當儘可能應用在一小節代碼中,或者少數幾個類、包中。這種方法其實必定程度上限制了對總體應用程序的性能分析,僅適合在程序單元中使用,點對點,針對性較強的分析,採用檢測分析的時候,更多時間要求開發人員明確知道哪裏有可能產生性能瓶頸。
線程阻塞不必定就是代碼編寫而產生的,發生線程阻塞時,更多的建議是去想,去看爲何會被阻塞,而不是直接查看代碼。儘可能採用線程執行時間軸的分析方法。
本地分析提供了既能夠深刻查看 JVM 內部,同時也能夠查看應用程序代碼執行的狀況。
若是本地分析顯示在 GC 過程當中大量的使用 CPU 資源,那麼調優收集器就是必要的。須要提醒你們的是,編譯線程一般是不影響應用程序的性能。