1. 程式人生 > >JVM除錯常用命令——jstack命令與Java執行緒棧(2)

JVM除錯常用命令——jstack命令與Java執行緒棧(2)

(接上文《JVM除錯常用命令——jstack命令與Java執行緒棧(1)》)

1.2、jstack中的執行緒關鍵資訊

上一篇文章中我們介紹了jstack命令的基本使用,也列舉了一個比較簡單的示例。雖然之前的文章內容中沒有介紹查詢結果中的一些關鍵資訊,但是這並不影響什麼。本片文章中我們將結合之前講過的執行緒狀態切換,對jstack命令的結果進行講解。

1.2.1、方法棧資訊及持有鎖資訊

方法棧實際上是執行緒棧原子物件“棧幀”中的資訊概要(“棧幀”內容將在後文中進行介紹),如下內容片段圖例中,是一部分連續的方法棧資訊:

在這裡插入圖片描述

  • 1、這是一個名叫http-nio-5700-Acceptor-0的執行緒(這個執行緒是筆者從Spring boot執行例項上擷取的,用於使用NIO模型接收5700埠上的Http請求),當前這個執行緒處於RUNNABLE可執行狀態(為什麼是可執行,而不是執行狀態呢?這個問題將在後文中解答)。

  • 2、上文已經提到執行緒棧dump資訊中,有個重要資訊就是方法棧資訊,方法棧實際上是“棧幀”資訊的概要,它以“at”關鍵字開頭。例如上圖示例中,該執行緒的方法呼叫過程在Thread類中的第748行開始呼叫另一個方法(看過原始碼的朋友可以知道,第748行程式碼就是執行Thread中target物件——一個實現了Runnable介面例項的run()方法)。緊接著在NioEndpoint$Acceptor類中的第453行(既是在NioEndpoint$Acceptor.run方法中),呼叫下一個方法。

  • 3、請注意在方法棧中的第三個方法呼叫過程中,該執行緒持有了一個物件的操作權(俗稱“鎖”),這個物件是一個“java.lang.Object”類的例項。那麼除非這個物件通過某種方式釋放掉這個物件的操作權,否則一旦其它執行緒(記為B)需要操作這個物件,並試圖獲得這個物件的“鎖”時,這個執行緒B就會進入“阻塞”狀態。接著我們再看一個阻塞狀態的執行緒棧例項,如下圖所示:

在這裡插入圖片描述

這是一個名為"http-nio-5700-exec-1"的執行緒,實際上這是“http-nio-5700-Acceptor-0”執行緒拿到準備好後的http處理請求後,緊接著實際進行http請求處理的所謂“執行執行緒”。這個執行緒方法棧底部的方法區就不逐一進行介紹了,熟悉java原生執行緒池的朋友都可以看到,這明顯是將處理任務送入了一個java執行緒池中然後進出處理(另外還可以看到這個執行緒池所使用的任務佇列是LinkedBlockingQueue)。

緊接著我們注意到,當前執行緒處於阻塞狀態。阻塞點在哪裡呢?請注意方法棧棧頂的兩個方法:當前執行緒在執行LockSupport類的第175行時(在LockSupport.park方法中),呼叫了一個JNI方法“Unsafe.park”,在這個後者內部當前執行緒等待記憶體地址起始點為“0x00000007853d5748”的物件的操作許可權——這個物件是AbstractQueuedSynchronizer$ConditionObject類的一個物件。於是執行緒進入阻塞狀態。

最後我們再看一個更簡單的,讓執行緒進入“阻塞”狀態的例項:

在這裡插入圖片描述

很顯然,以上執行緒進入了阻塞狀態——而且是一個有時間約束的阻塞。原因是當前執行緒在執行“AbstractProtocol$AsyncTimeout”類中的第1200行時(在AbstractProtocol$AsyncTimeout.run方法體內),呼叫了“Thread.sleep”方法。

1.2.2、執行緒狀態資訊

是的,通過以上小節的講解我們知道了,執行緒狀態是執行緒dump資訊中非常重要的資訊項。並且彷彿根據進入阻塞的原因(呼叫方式)不一樣,同樣的“阻塞”狀態都還有一些細小的差別。為了能夠看懂jstack命令給出的執行緒棧dump資訊中執行緒的狀態描述,我們需要首先介紹一下執行緒的狀態分類表述。

  • RUNNABLE, 在虛擬機器內執行的。執行中狀態。注意,處於這樣的執行緒可能在其方法棧中還能看到locked關鍵字,這表明它獲得了某個物件的操作許可權,並繼續進行著後續的處理過程。

  • BLOCKED, 試圖獲得指定物件的操作許可權,並進入synchronized同步塊,但是由於某種原因當前執行緒還沒有獲得指定的操作操作許可權,還在synchronized同步塊外處於阻塞狀態。

  • WATING, 一種無限期阻塞狀態,等待另一個執行緒執行特定操作。等待某個condition或monitor發生,一般停留在park(), wait(), sleep(),join() 等語句裡。根據進入的方式不一樣,阻塞狀態下還有一些細微的差別,這個問題我們將在下一節進行詳細講解。這裡特別要注意BLOCKED和WATING/TIMED_WATING的區別,這個區別將在後文進行詳細講解

  • TIMED_WATING, 有時限的等待另一個執行緒的特定操作。例如,和WAITING的區別可能是wait() 等語句加上了時間限制 wait(timeout),也可能是呼叫了sleep方法。

  • TERMINATED,執行緒已退出的。

實際上以上的幾個狀態在Java原始碼中都已經做了非常詳細的說明,如下所示:

在這裡插入圖片描述

  • 那麼按照常理來說,當執行緒處於執行狀態時,執行緒的標識應該是RUN、RUNING等單詞,但為什麼java官方的描述中,對於處理執行狀態的執行緒卻只有RUNNABLE這樣的標識呢

這是因為執行緒和執行緒排程都是作業系統級別的概念,某一個執行緒是否由CPU進行執行,是無法由開發者、應用程式使用者決定的,甚至不是由JVM決定的(JVM只對執行緒優先順序、執行緒排程型別的選擇提供支援),而是由作業系統決定。而由於Java的跨平臺性,所以在JVM中有專門的執行緒排程程式(模組)來配合不同作業系統中執行緒的呼叫方式完成執行緒排程。作為JVM來說只能通過Native的方式將執行緒狀態變更為“可執行”(包括設定優先順序),然後由作業系統來決定具體執行哪一個執行緒。所以JVM中對於處理執行狀態的執行緒,其標識都是“RUNNABLE”。

2、執行緒狀態及切換方式

2.1、狀態切換

2.1.1、從New狀態進入Runnable狀態

當用new操作符建立一個執行緒時, 例如new Thread®。這時執行緒還沒有執行,那麼它就處於新建狀態。新建狀態不能通過jstack命令進行跟蹤,原因是當前執行緒還沒有由託管到作業系統,但是我們可以通過Java程式碼調出當前執行緒的狀態資訊,如下圖所示:

在這裡插入圖片描述

可以看到主執行緒還沒有執行thread1.start()方法,這時列印的執行緒狀態資訊就是:

當前threa1執行緒狀態為:NEW
當前threa2執行緒狀態為:NEW

2.1.2、從Runnable狀態進入BLOCKED狀態

上文已經提到當某一個執行緒需要得到指定的Object的操作權,並試圖進入synchronized同步塊,但發現不能獲得Object的操作權時,就會進入BLOCKED狀態。請看如下程式碼和除錯效果:

在這裡插入圖片描述

上圖中有建立了兩個使用者執行緒,名稱分別為“thread1”和“thread2”。因為其中thread2先於thread1拿到了TestStates.class這個物件的操作權(基礎知識:class也是物件),並進入synchronized同步塊(參見除錯資訊);所以thread1進入BLOCKED狀態,我們使用jstack資訊觀察當前Java程序執行緒棧的狀態可發現如下圖所示的情況:

# jstack 197300
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000002fce000 nid=0x3b794 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"thread2" #15 prio=5 os_prio=0 tid=0x000000001ee53000 nid=0x368d8 runnable [0x000000001febf000]
   java.lang.Thread.State: RUNNABLE
        at testThread.TestStates$MyThread.run(TestStates.java:23)
        - locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
        at java.lang.Thread.run(Thread.java:748)
"thread1" #14 prio=5 os_prio=0 tid=0x000000001ee52000 nid=0x3b80c waiting for monitor entry [0x000000001fdbf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at testThread.TestStates$MyThread.run(TestStates.java:23)
        - waiting to lock <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
        at java.lang.Thread.run(Thread.java:748)

.........後面的資訊省去

可以看到名叫thread1的執行緒正在等待獲取物件(0x000000076c5c4090)的操作許可權,後者是一個java.lang.Class類的例項。而目前持有當前物件(0x000000076c5c4090)操作許可權的執行緒是名為thread2的執行緒。

2.1.3、從Runnable狀態進入WAITING狀態(多種方式)

執行緒從可執行狀態切換到WAITING狀態的方式就很多了。例如:呼叫wait方法,釋放物件操作權、當前執行緒呼叫目前執行緒的join方法,等待後者執行完成、當前執行緒呼叫sleep方法、可重入鎖(包括讀寫分離鎖)呼叫lock方法,並觸發阻塞效果、呼叫LockSupport的park方法,自旋當前執行緒或指定的物件等等,下文我們將要對這些細節進行詳細描述。

2.1.3.1、呼叫wait方法,釋放物件操作權

首先介紹一組最簡單的方式,就是當前在synchronized同步塊中的執行緒呼叫wait()方法,讓出被鎖定物件的操作許可權。如下示例:

在這裡插入圖片描述

在以上程式碼中,雖然執行緒thread1先於thread2拿到TestStates.class物件的操作權,並唯一進入synchronized同步塊。但是在synchronized同步塊中,執行緒thread1呼叫了wait方法讓出了操作權。這時還未進圖synchronized同步塊的執行緒thread2就可以拿到操作權(因為在synchronized同步塊外只有thread2再等待獲得資源的操作權)。這時執行緒thread1就進入了WAITING狀態,而thread2由BLOCKED狀態變成了Runnable狀態。通過jstack命令,我們可以驗證到這樣的狀態切換:

# jstack 246124
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"thread2" #15 prio=5 os_prio=0 tid=0x000000001ef74800 nid=0x3c554 runnable [0x000000001ffdf000]
   java.lang.Thread.State: RUNNABLE
        at testThread.TestStates$MyThread.run(TestStates.java:22)
        - locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
        at java.lang.Thread.run(Thread.java:748)
"thread1" #14 prio=5 os_prio=0 tid=0x000000001ef73800 nid=0x3c5cc in Object.wait() [0x000000001fedf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
        at java.lang.Object.wait(Object.java:502)
        at testThread.TestStates$MyThread.run(TestStates.java:24)
        - locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
        at java.lang.Thread.run(Thread.java:748)

通過jstack命令我們觀察到:執行緒thread1的執行緒棧中,從棧底開始的第二個棧幀拿到了物件(0x000000076c5c4090)的操作許可權,並執行到TestStates$MyThread類中的第24行(既是在run方法中)。接著在24行呼叫了Object類中的wait方法(Object類中的第502行),最後通過呼叫JNI中的wait方法,重新等待獲取物件(0x000000076c5c4090)的操作權。

而執行緒thread2拿到了物件(0x000000076c5c4090)的操作權,並正在執行TestStates$MyThread類中的第22行程式碼(同樣在TestStates$MyThread類的run方法中,既是那句System.out)。這裡需要特別注意,當正在執行的執行緒由於作業系統的原因執行非常“卡頓”,例如作業系統的磁碟I/O操作、作業系統的網路I/O操作,那麼我們通過jstack命令觀察時,還是會發現當前執行緒處於“RUNNABLE”狀態。

請注意這個WAITING狀態的備註說明“java.lang.Thread.State: WAITING (on object monitor)”,這非常關鍵,這告訴我們該執行緒進入WAITING狀態的原因是物件監視器造成——具體來說是物件監視器發現當前執行緒thread1已經讓出了操作許可權。

=================================
(接下文)