給一個系統定位問題的時候,知識、經驗是關鍵基礎,數據是依據,工具是運用知識處理數據的手段。這裏的數據包括:運行日誌、異常堆棧、GC日誌、線程快照(threaddump/javacore文件)、堆轉儲快照(heapdump/hprof文件)等。經常使用適當的虛擬機監控和分析的工具可以加快我們分析數據和定位解決問題的速度,但我們在學習工具前,也應當意識到工具永遠都是知識技能的一層包裝,沒有什麼工具是“祕密武器”,學會了就能包醫百病。
JDK的命令行工具
Java開發人員肯定都知道JDK的bin目錄下有“java.exe“和”javac.exe“這兩個命令行工具,但並非所有程序員都瞭解過JDK的bin下其他命令行程序的作用。每逢JDK更新版本之時,bin目錄下命令行工具的數量和功能總會不知不覺地增加和增強。
作者介紹了這些工具中的一部分,主要用於監視虛擬機和故障處理的工具。在軟件的使用說明中這些故障處理工具被聲明爲”沒有技術支持並且是實驗性質的“(unsupported and experimental)的產品,但事實上這些工具都非常穩定並且功能強大,能在處理應用程序性能問題、定位故障時發揮很大的作用。
這些工具體積都異常的小,大多都在30KB左右。並非JDK開發團隊刻意將他們製作得如此精煉來炫技,而是這些命令行工具大多數是jdk\lib\tools.jar類庫的一層薄封裝而已。它們主要的功能代碼是在tools類庫中實現的。
JDK團隊選擇採用Java代碼來實現這些監控工具是有特別用意的:當應用程序部署到生產環境後,無論是直接接觸物理服務器還是遠程Telnet到服務器上都可能會受到限制。藉助tools.jar類庫裏面的接口,我們可以直接在應用程序中實現功能強大的監控分析功能。
本章介紹的工具全部基於windows平臺下的JDK 1.6,版本和操作系統不同會有不同。
表 Sun JDK監控和故障處理工具
名稱 | 主要功能 |
jps | JVM Process Status Tool,顯示指定系統內所有HotSpot虛擬機進程 |
jstat | JVM Statistics Minitoring Tool,用於收集HotSpot虛擬機各方面的運行數據 |
jinfo | Configuration Info for Java,顯示虛擬機配置信息 |
jmap | Memory Map for Java,生成虛擬機的內存轉儲快照(heapdump)文件 |
jhat | JVM Heap Dump Browser,用於分析heapdump文件,它會建立一個HTTP/HTML服務器,讓用戶可以在瀏覽器上查看分析結果 |
jstack | Stack Trace for Java,顯示虛擬機的線程快照 |
jps:虛擬機進程狀況工具
JDK的很多小工具的名稱都參考了Unix命令的命名方式,jps(JVM Process Status Tool)是其中的典型。除了名字像Unix的ps命令外,功能也和ps類似:可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類(Main Class,main()函數所在的類)的名稱,以及這些進程的本地虛擬機的唯一ID(LVMID,Local Virtual Machine Identifier)。雖然功能比較單一,但它是使用頻率最高的JDK命令行工具,因爲其他JDK工具大多需要輸入它查詢到的LVMID來確定要監控的是哪一個虛擬機進程。對於本地虛擬機進程來說,LVMID與操作系統的進程ID(PID,Process Identifier)是一致的,使用Windows的任務管理器或Unix的ps命令也可以查詢到虛擬機進程的LVMID,但如果使用了多個虛擬機進程,無法根據進程名稱定位時,那就只能依賴jps命令顯示主類的功能區才能區分了。
jps命令格式:
jps [option] [hostid]
jps執行樣例:
C:\Users\Administrator>jps -l
5828 D:\Work\Develop\SpringToolSuite\springsource\sts-3.3.0.RELEASE\\plugins/org
.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
5680 sun.tools.jps.Jps
jps可以通過RMI協議查詢開啓了RMI服務的遠程虛擬機進程狀態,hostid爲RMI註冊表中註冊的主機名。jps的其他常用選項見下表
選項 | 作用 |
-q | 只輸出LVMID,省略主類的名稱 |
-m | 輸出虛擬機進程啓動時傳遞給主類的main()函數的參數 |
-l | 輸出主類的全名,如果進程執行的是jar包,輸出jar路徑 |
-v | 輸出虛擬機進程啓動時JVM參數 |
jstat:虛擬機統計信息監控工具
jstat(JVM Statistics Monitoring Tool)是用於監控虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,在沒有GUI圖像界面,只提高了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。
jstat命令格式:
jstat [option vmid [interval[s|ms] [count]] ]
對於命令格式中的VMID與LVMID需要特別說明下:如果是本地虛擬機進程,VMID和LVMID是一致的,如果是遠程虛擬機進程,那VMID的格式應當是:
[protocol:][//] lvmid [@hostname[:port]/servername]
參數interval和count代表查詢間隔和次數,如果省略這兩個參數,說明只查詢一次。假設需要每250毫秒查詢一次進程5828垃圾收集狀況,一共查詢5次,那命令行如下:
jstat -gc 5828 250 5
選項option代表這用戶希望查詢的虛擬機信息,主要分爲3類:類裝載、垃圾收集和運行期編譯狀況,具體選項及租用參見下表:
選項 | 作用 |
-class | 監視類裝載、卸載數量、總空間及類裝載所耗費的時間 |
-gc | 監視Java堆狀況,包括Eden區、2個Survivor區、老年代、永久代等的容量 |
-gccapacity | 監視內容與-gc基本相同,但輸出主要關注Java堆各個區域使用到的最大和最小空間 |
-gcutil | 監視內容與-gc基本相同,但輸出主要關注已使用空間佔總空間的百分比 |
-gccause | 與-gcutil功能一樣,但是會額外輸出導致上一次GC產生的原因 |
-gcnew | 監視新生代GC的狀況 |
-gcnewcapacity | 監視內容與-gcnew基本相同,輸出主要關注使用到的最大和最小空間 |
-gcold | 監視老年代GC的狀況 |
-gcoldcapacity | 監視內容與——gcold基本相同,輸出主要關注使用到的最大和最小空間 |
-gcpermcapacity | 輸出永久代使用到的最大和最小空間 |
-compiler | 輸出JIT編譯器編譯過的方法、耗時等信息 |
-printcompilation | 輸出已經被JIT編譯的方法 |
舉個例子:
C:\Users\Administrator>jstat -gcutil 5828
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 1.82 52.18 99.91 329 4.894 269 80.244 85.139
這是我監控到我的eclipse的內存狀況。查詢結果表明:新生代Eden區(E,表示Eden)使用了1.82%的空間,兩個Survivor區(S0、S1,表示Survivor0、Survivor1)裏面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)則分別使用了52.18%和99.91%的空間。程序運行以來共發生Minor GC(YGC,Young GC)329次,總耗時(YGCT,Young GC Time)4.894秒,發生Full GC(FGC)269次,總耗時(FGCT)80.244秒,所有GC總耗時(GCT)85.139秒。
使用jstat工具在純文本狀態下監視虛擬機狀態的變化,確實不如後面將會提到的VisualVM等可視化的監視工具直接以圖表展現的那樣直觀。但很多服務器管理員都習慣了在文本控制檯共組哦,直接在控制檯中使用jstat命令已然是一種常用 的監控方式。
jinfo: Java配置信息工具
jinfo(Configuration Info for Java)的作用是實時地查看和調整虛擬機的各項參數。使用jps的命令的-v參數可以查看虛擬機啓動時顯示指定的參數列表,但如果想知道未被顯示指定的參數的系統默認值,除了去找資料外,就只能使用jinfo的-flag選項進行查詢了(如果只限於JDK1.6或以上版本的話,使用java -XX:+PrintFlagsFinal查看參數默認值也是一個很好的選擇),jinfo還可以使用-sysprops選項把虛擬機進程的System.getProperties()的內容打印出來。這個命令在JDK1.5時期已經隨着Linux版的JDK發佈,當時只提供了信息查詢的功能,JDK1.6之後,jinfo在Windows和Linux平臺都有提供,並且加入了運行期修改參數的能力,可以使用-flag[+|-]name或-flag name=valule修改一部分運行期可寫的虛擬機參數值。JDK1.6中,jinfo對於Windows平臺的功能仍然有較大的限制,只提供了最基本的-flag選項。
jinfo命令格式:
jinfo [option] pid
執行樣例:查詢CMSInitiatingOccupancyFraction參數值。
C:\Users\Administrator>jinfo -flag CMSInitiatingOccupancyFraction 5828
-XX:CMSInitiatingOccupancyFraction=-1
jmap: Java內存映像工具
jmap(Memory Map for Java)命令用於生產堆轉儲快照(一般稱爲heapdump或dump文件)。如果不使用jmap命令,要向獲取Java堆轉儲快照還有一些比較”暴力“的手段:譬如-XX:+HeapDumpOnOutOfMemoryError參數,可以讓虛擬機在OOM異常出現之後自動生生成dump文件,通過-XX:+HeapDumpOnCtrlBreak參數則可以使用[Ctrl]+[Break]鍵讓虛擬機生成dump文件,又或者在Linux系統下通過Kill -3命令發送進程退出信號”恐嚇“一下虛擬機,也能拿到dump文件。
jmap的作用並不僅僅是爲了獲取dump文件,它還可以查詢finalize執行隊列,Java堆和永久代的詳細信息,如空間使用率、當前用的是那種收集器等。
和jinfo命令一樣,jmap有不少功能在Windows平臺下是受限的,除了生成dump文件的-dump選項和用於查看每個類的實例、空間佔用統計的-histo選項所有操作系統都提供外,其餘選項只能在Linux/Solaris下使用。
jmap命令格式:
jmap [option] vmid
option選項合法值與具體含義:
選項 | 作用 |
-dump | 生成Java堆轉儲快照。格式爲:-dump:[live,]format=b,file=<filename>,其中live子參數說明是否只dump出存活的對象 |
-finalizerinfo | 顯示在F-Queue中等待Finalizer線程執行finalize()方法的對象。只在Linux/Solaris平臺下有效 |
-heap | 顯示Java堆詳細信息,如使用哪種回收器、參數配置、分代狀況等。只在Linux/Solaris平臺下有效 |
-histo | 顯示堆中對象統計信息,包括類、實例數量和合計容量 |
-permstat | 以ClassLoader爲統計口徑顯示永久代內存狀態。只在Linux/Solaris平臺下有效 |
-F | 當虛擬機進程對-dump選項沒有響應時,可使用這個選項強制生成dump快照。只在Linux/Solaris平臺下有效 |
樣例:
C:\Users\Administrator>jmap -dump:format=b,file=eclipse.bin 5828
Dumping heap to C:\Users\Administrator\eclipse.bin ...
Heap dump file created
這是使用jmap生成一個正在運行的Eclipse的dump快照文件的例子,5828爲jps查詢到的LVMID。
jhat:虛擬機堆轉儲快照分析工具
Sun JDK提供了jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果後,可以在瀏覽器中查看,不過實事求是地說,在實際工作中,除非真的沒有別的工具可用,否則一般不會去直接使用jhat命令來分析demp文件,主要原因有二:意識一般不會在部署應用程序的服務器上直接分析dump文件,即使可以這樣做,也會盡量將dump文件拷貝到其他機器上進行分析,因爲分析工作時一個耗時且消耗硬件資源的過程,既然都要在其他機器上進行,就沒必要收到命令行工具的限制了。另外一個原因是jhat的分析功能相對來說很簡陋,VisualVM以及專門分析dump文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能實現比jhat更強大更專業的分析功能。
C:\Users\Administrator>jhat eclipse.bin
Reading from eclipse.bin...
Dump file created Tue Aug 19 16:01:10 CST 2014
Snapshot read, resolving...
Resolving 2126401 objects...
Chasing references, expect 425 dots...................................
......................................................................
......................................................................
......................................................................
......................................................................
............................................................
Eliminating duplicate references......................................
......................................................................
......................................................................
......................................................................
......................................................................
.........................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
"Server is ready."出現後就可以在瀏覽器中鍵入http://localhost:7000 查看分析結果了。
分析結果默認以包爲單位進行分組顯示,分析內存泄露問題主要會使用到其中的”Heap Histogram“(與jmap-hosto功能一樣)與OQL頁籤的功能。前者可以找到內存總容量最大的對象,後者是標準的對象查詢語句,使用類似SQL的語法對內存中的對象進行查詢統計。
jstack: Java堆棧跟蹤工具
jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照(一般稱爲threaddump或javacore文件)。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在後臺做些什麼事情,或者等待着什麼資源。
jstack命令格式:
jstack [option] vmid
option選項的合法值與具體意義如下:
選項 | 作用 |
-F | 當正常輸出的請求不被響應時,強制輸出線程堆棧 |
-l | 除堆棧外,顯示關於鎖的附加信息 |
-m | 如果調用到本地方法的話,可以顯示C/C++的堆棧 |
實例(用jstack查看eclipse線程堆棧):
C:\Users\Administrator>jstack -l 5828
運行結果太長,不截取了。
在JDK1.5中,java.lang.Thread類新增了一個getAllStackTraces()方法用於獲取虛擬機中所有線程的StackTraceElement對象。使用這個方法可以簡單的幾行代碼就完成jstack的大部分功能,在實際項目中不妨調用這個方法做個管理員頁面,可以隨時使用瀏覽器來查看線程堆棧,代碼如下:
<%@ page import="java.util.Map"%>
<html>
<head>
<title>服務器線程信息</title>
</head>
<body>
<pre>
<%
for(Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()){
Thread thread = (Thread)stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[])stackTrace.getValue();
if(thread.equals(Thread.currentThread())){
continue;
}
out.print("\n線程:"+thread.getName()+"\n");
for(StackTraceElement element : stack){
out.print("\t"+element+"\n");
}
}
%>
</pre>
</body>
</html>