JVM Thread Dump日誌結構解析

這篇文章首先對Thread Dump日誌文件的結構進行分析。

目錄 [隱藏]

一個典型的thread dump文件主要由一下幾個部分組成:


上圖將JVM上的線程堆棧信息和線程信息做了詳細的拆解。

第一部分:Full thread dump identifier

這一部分是內容最開始的部分,展示了快照文件的生成時間和JVM的版本信息。

2017-10-19 10:46:44
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

第二部分:Java EE middleware, third party & custom application Threads

這是整個文件的核心部分,裏面展示了JavaEE容器(如tomcat、resin等)、自己的程序中所使用的線程信息。這一部分詳細的含義見 Java內存泄漏分析系列之四:jstack生成的Thread Dump日誌線程狀態分析

"resin-22129" daemon prio=10 tid=0x00007fbe5c34e000 nid=0x4cb1 waiting on condition [0x00007fbe4ff7c000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:315)
    at com.caucho.env.thread2.ResinThread2.park(ResinThread2.java:196)
    at com.caucho.env.thread2.ResinThread2.runTasks(ResinThread2.java:147)
    at com.caucho.env.thread2.ResinThread2.run(ResinThread2.java:118)

第三部分:HotSpot VM Thread

這一部分展示了JVM內部線程的信息,用於執行內部的原生操作。下面常見的集中內置線程:

"Attach Listener"

該線程負責接收外部命令,執行該命令並把結果返回給調用者,此種類型的線程通常在桌面程序中出現。

"Attach Listener" daemon prio=5 tid=0x00007fc6b6800800 nid=0x3b07 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM"

執行main()的線程在執行完之後調用JNI中的 jni_DestroyJavaVM() 方法會喚起DestroyJavaVM 線程。在JBoss啓動之後,也會喚起DestroyJavaVM線程,處於等待狀態,等待其它線程(java線程和native線程)退出時通知它卸載JVM。

"DestroyJavaVM" prio=5 tid=0x00007fc6b3001000 nid=0x1903 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread"

用於啓動服務的線程

"Service Thread" daemon prio=10 tid=0x00007fbea81b3000 nid=0x5f2 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"CompilerThread"

用來調用JITing,實時編譯裝卸CLASS。通常JVM會啓動多個線程來處理這部分工作,線程名稱後面的數字也會累加,比如CompilerThread1。

"C2 CompilerThread1" daemon prio=10 tid=0x00007fbea814b000 nid=0x5f1 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=10 tid=0x00007fbea8142000 nid=0x5f0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher"

Attach Listener線程的職責是接收外部jvm命令,當命令接收成功後,會交給signal dispather 線程去進行分發到各個不同的模塊處理命令,並且返回處理結果。
signal dispather線程也是在第一次接收外部jvm命令時,進行初始化工作。

"Signal Dispatcher" daemon prio=10 tid=0x00007fbea81bf800 nid=0x5ef runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer"

這個線程也是在main線程之後創建的,其優先級爲10,主要用於在垃圾收集前,調用對象的finalize()方法;關於Finalizer線程的幾點:
(1)只有當開始一輪垃圾收集時,纔會開始調用finalize()方法;因此並不是所有對象的finalize()方法都會被執行;
(2)該線程也是daemon線程,因此如果虛擬機中沒有其他非daemon線程,不管該線程有沒有執行完finalize()方法,JVM也會退出;
(3)JVM在垃圾收集時會將失去引用的對象包裝成Finalizer對象(Reference的實現),並放入ReferenceQueue,由Finalizer線程來處理;最後將該Finalizer對象的引用置爲null,由垃圾收集器來回收;
(4)JVM爲什麼要單獨用一個線程來執行finalize()方法呢?
如果JVM的垃圾收集線程自己來做,很有可能由於在finalize()方法中誤操作導致GC線程停止或不可控,這對GC線程來說是一種災難。

"Finalizer" daemon prio=10 tid=0x00007fbea80da000 nid=0x5eb in Object.wait() [0x00007fbeac044000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
    - locked <0x00000006d173c1a8> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler"

JVM在創建main線程後就創建Reference Handler線程,其優先級最高,爲10,它主要用於處理引用對象本身(軟引用、弱引用、虛引用)的垃圾回收問題 。

"Reference Handler" daemon prio=10 tid=0x00007fbea80d8000 nid=0x5ea in Object.wait() [0x00007fbeac085000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:503)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
    - locked <0x00000006d173c1f0> (a java.lang.ref.Reference$Lock)

"VM Thread"

JVM中線程的母體,根據HotSpot源碼中關於vmThread.hpp裏面的註釋,它是一個單例的對象(最原始的線程)會產生或觸發所有其他的線程,這個單例的VM線程是會被其他線程所使用來做一些VM操作(如清掃垃圾等)。
在 VM Thread 的結構體裏有一個VMOperationQueue列隊,所有的VM線程操作(vm_operation)都會被保存到這個列隊當中,VMThread 本身就是一個線程,它的線程負責執行一個自輪詢的loop函數(具體可以參考:VMThread.cpp裏面的void VMThread::loop()) ,該loop函數從VMOperationQueue列隊中按照優先級取出當前需要執行的操作對象(VM_Operation),並且調用VM_Operation->evaluate函數去執行該操作類型本身的業務邏輯。
VM操作類型被定義在vm_operations.hpp文件內,列舉幾個:ThreadStop、ThreadDump、PrintThreads、GenCollectFull、GenCollectFullConcurrent、CMS_Initial_Mark、CMS_Final_Remark….. 有興趣的同學,可以自己去查看源文件。

"VM Thread" prio=10 tid=0x00007fbea80d3800 nid=0x5e9 runnable

第四部分:HotSpot GC Thread

JVM中用於進行資源回收的線程,包括以下幾種類型的線程:

"VM Periodic Task Thread"

該線程是JVM週期性任務調度的線程,它由WatcherThread創建,是一個單例對象。該線程在JVM內使用得比較頻繁,比如:定期的內存監控、JVM運行狀況監控。

"VM Periodic Task Thread" prio=10 tid=0x00007fbea82ae800 nid=0x5fa waiting on condition

可以使用jstat 命令查看GC的情況,比如查看某個進程沒有存活必要的引用可以使用命令 jstat -gcutil <pid> 250 7 參數中pid是進程id,後面的250和7表示每250毫秒打印一次,總共打印7次。
這對於防止因爲應用代碼中直接使用native庫或者第三方的一些監控工具的內存泄漏有非常大的幫助。

"GC task thread#0 (ParallelGC)"

垃圾回收線程,該線程會負責進行垃圾回收。通常JVM會啓動多個線程來處理這個工作,線程名稱中#後面的數字也會累加。

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fc6b480d000 nid=0x2503 runnable

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fc6b2812000 nid=0x2703 runnable

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fc6b2812800 nid=0x2903 runnable

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fc6b2813000 nid=0x2b03 runnable

如果在JVM中增加了 -XX:+UseConcMarkSweepGC 參數將會啓用CMS (Concurrent Mark-Sweep)GC Thread方式,以下是該模式下的線程類型:

"Gang worker#0 (Parallel GC Threads)"

原來垃圾回收線程GC task thread#0 (ParallelGC) 被替換爲 Gang worker#0 (Parallel GC Threads)。Gang worker 是JVM用於年輕代垃圾回收(minor gc)的線程。

"Gang worker#0 (Parallel GC Threads)" prio=10 tid=0x00007fbea801b800 nid=0x5e4 runnable 

"Gang worker#1 (Parallel GC Threads)" prio=10 tid=0x00007fbea801d800 nid=0x5e7 runnable 

"Concurrent Mark-Sweep GC Thread"

併發標記清除垃圾回收器(就是通常所說的CMS GC)線程, 該線程主要針對於年老代垃圾回收。

"Concurrent Mark-Sweep GC Thread" prio=10 tid=0x00007fbea8073800 nid=0x5e8 runnable 

"Surrogate Locker Thread (Concurrent GC)"

此線程主要配合CMS垃圾回收器來使用,是一個守護線程,主要負責處理GC過程中Java層的Reference(指軟引用、弱引用等等)與jvm 內部層面的對象狀態同步。

"Surrogate Locker Thread (Concurrent GC)" daemon prio=10 tid=0x00007fbea8158800 nid=0x5ee waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

這裏以 WeakHashMap 爲例進行說明,首先是一個關鍵點:

  • WeakHashMap和HashMap一樣,內部有一個Entry[]數組;
  • WeakHashMap的Entry比較特殊,它的繼承體系結構爲Entry->WeakReference->Reference;
  • Reference 裏面有一個全局鎖對象:Lock,它也被稱爲pending_lock,注意:它是靜態對象;
  • Reference 裏面有一個靜態變量:pending;
  • Reference 裏面有一個靜態內部類:ReferenceHandler的線程,它在static塊裏面被初始化並且啓動,啓動完成後處於wait狀態,它在一個Lock同步鎖模塊中等待;
  • WeakHashMap裏面還實例化了一個ReferenceQueue列隊

假設,WeakHashMap對象裏面已經保存了很多對象的引用,JVM 在進行CMS GC的時候會創建一個ConcurrentMarkSweepThread(簡稱CMST)線程去進行GC。ConcurrentMarkSweepThread線程被創建的同時會創建一個SurrogateLockerThread(簡稱SLT)線程並且啓動它,SLT啓動之後,處於等待階段。
CMST開始GC時,會發一個消息給SLT讓它去獲取Java層Reference對象的全局鎖:Lock。直到CMS GC完畢之後,JVM 會將WeakHashMap中所有被回收的對象所屬的WeakReference容器對象放入到Reference 的pending屬性當中(每次GC完畢之後,pending屬性基本上都不會爲null了),然後通知SLT釋放並且notify全局鎖:Lock。此時激活了ReferenceHandler線程的run方法,使其脫離wait狀態,開始工作了。
ReferenceHandler這個線程會將pending中的所有WeakReference對象都移動到它們各自的列隊當中,比如當前這個WeakReference屬於某個WeakHashMap對象,那麼它就會被放入相應的ReferenceQueue列隊裏面(該列隊是鏈表結構)。 當我們下次從WeakHashMap對象裏面get、put數據或者調用size方法的時候,WeakHashMap就會將ReferenceQueue列隊中的WeakReference依依poll出來去和Entry[]數據做比較,如果發現相同的,則說明這個Entry所保存的對象已經被GC掉了,那麼將Entry[]內的Entry對象剔除掉。

第五部分:JNI global references count

這一部分主要回收那些在native代碼上被引用,但在java代碼中卻沒有存活必要的引用,對於防止因爲應用代碼中直接使用native庫或第三方的一些監控工具的內存泄漏有非常大的幫助。

JNI global references: 830

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章