jstack(Stack Trace For Java, 官方鏈接)用于生成java虛擬機某個進程在當前時刻的線程快照(一般稱為threaddump或javacore文件,由線程的調用堆棧組成),用來定位線程長時間停頓的原因,如死循環、死鎖等。
一般在用該工具時主要分為三步:
-
獲取進程id
方法1:
jps -l
方法2:
ps -ef|grep java
方法3:
lsof -i:<port>
上述三個方法根據具體情況選擇使用,方法1最簡便(適合java進程少的情況),方法2信息更詳盡,方法3則適合有多個java進程,根據方法1可能分辨不出來想找的進程,而需要通過端口號進行定位。
-
獲取進程中的線程狀態
top -Hp <pid>
注意該命令只能在linux中使用,而在macOS上不能使用。因為linux將線程作為輕量級進程也分配了pid,而macOS并沒有這么處理。
如果通過上述命令我們發現進程中的某個線程指標不正常,想重點關注,這時需要將線程的pid通過下面命令轉化為十六進進制,方便在線程快照信息中查找。
? echo 'obase=16; ibase=10; <pid>' | bc | tr '[A-Z]' '[a-z]'
-
查看并分析線程快照信息
用
jstack <pid>
命令可以在控制臺打印線程快照信息,jstack <pid> > <path>
可以將線程快照信息輸出到文件。如下為打印出來的一條線程快照,由10個部分組成。image-20181111102804259- 線程名稱,如果程序中沒有顯示給線程命名則顯示默認名稱
- 線程序號,相當于程序所有線程的一個編號
- 線程優先級,java中線程的默認優先級為5
- 線程系統優先級
- 線程id
- 線程native id,在linux中對應線程的輕量級進程id,十六進制,通過該字段都與top命令中的線程對應起來。
- 線程描述
- 線程棧的起始地址
- 線程狀態
- 線程執行堆棧,具體到代碼的行數
需要注意的是執行該命令時當前用戶必須為啟動該進程的用戶,否則會失敗,即使是root用戶也不行。
接下來將通過兩個實例來對該工具的使用進行詳細說明:
實例1: 利用線程快照分析CPU占用過高的原因
用于演示CPU示例代碼如下: 啟動三個線程,且某個線程獲取唯一的鎖后一直沒有釋放,可想而知,另外兩個線程將處于阻塞狀態。
import java.util.UUID;
public class HighCPUCase {
public static Object lock = new Object();
public static void main(String[] args) {
new Thread(new Task(), "task1").start();
new Thread(new Task(), "task2").start();
new Thread(new Task(), "task3").start();
}
static class Task implements Runnable{
@Override
public void run(){
synchronized (lock){
while (true){
System.out.println(UUID.randomUUID().toString());
}
}
}
}
}
現在用文章開始的三步曲來驗證我們的猜想。
-
首先獲取進程id
image-20181110225814867 -
獲取進程中消耗CPU最高的線程
image-20181110230019853將23068用如下命令轉化為16進制后為5a1c
image-20181110230321690 -
查看并分析線程快照
image-20181110231223530? 上圖中藍色線框為我們需要重點關注的內容,可以看到5a1c(由第2步得到)對應的線程為task1,該線程處于RUNNABLE狀態,且下在執行HighCPUCase.java中的第17行,而線程task2和task3都因為等待鎖而處于BLOCKED狀態。仔細觀察可以發現task2和task3等待的鎖與task1獲得的鎖完全相同。
? 由此我們下結論,CPU高的原因是因為線程task1,而它正在執行HighCPUCase.java中的第17行,線程task2和task3處于阻塞狀態是因為task1獲得唯一一把鎖后一直沒有釋放。然后我們根據結論再去看代碼,分析得完全沒毛病。
實例2: 利用線程快照分析死鎖
下面為演示死鎖的代碼,模擬有兩個線程A,B, 兩把鎖1,2,當線程1成功拿到鎖1嘗試去拿鎖2發現拿不到,而此時線程2成功拿到鎖2嘗試去拿鎖1也拿不到,誰也不讓誰就只能干耗著了,導致程序一直不能結束。
public class DeadLockCase {
public static void main(String[] args){
Object o1 = new Object();
Object o2 = new Object();
new Thread(new SyncThread(o1, o2), "t1").start();
new Thread(new SyncThread(o2, o1), "t2").start();
}
static class SyncThread implements Runnable {
private Object lock1;
private Object lock2;
public SyncThread(Object o1, Object o2){
this.lock1 = o1;
this.lock2 = o2;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " acquiring lock on " + lock1);
synchronized (lock1) {
System.out.println(name + " acquired lock on " + lock1);
work();
System.out.println(name + " acquiring lock on " + lock2);
synchronized (lock2) {
System.out.println(name + " acquired lock on " + lock2);
work();
}
System.out.println(name + " released lock on " + lock2);
}
System.out.println(name + " released lock on " + lock1);
}
private void work() {
try {
//模擬死鎖的關鍵,保證線程1只能獲取一個鎖,而線程2能獲取到另一個鎖
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行上面代碼輸出如下,可以看到符合我們的預期:
下面還是通過線程快照來反推:
-
首先拿到進程id
image-20181110235201192 -
查看進程內的線程情況
image-20181110235336030可以看到所有線程無異常指標,這一步還不知道哪一個線程有問題。
23152對就的十六進制數為5a70, 23144對應的十六進制數為5a68,接下來就重點關注這兩個。
-
查看線程快照
開始部分:細心觀察發現兩個線程處于BLOCKED狀態是因為在等相互之間的鎖。
image-20181111000447280結尾部分:很明確的告訴了你發現了一個java級別死鎖,t2在待t1手上的鎖,t1在等t2手上的鎖。
在實際生產中肯定比這本文中涉及到的兩個例子復雜,但如果能先把基礎的學會,碰到問題也不會慌而是學會去分解復雜的問題,大而化小,最后方能解決問題。
參考文章: