如何分析Java線程Dumps

    1.簡介

    應用程序有時會掛起或運行緩慢,並且找出根本原因並不總是一件容易的事。線程Dump提供了一個運行Java程序的當前狀態的快照**。但是,生成的數據包括多個長文件。因此,我們需要分析Java thread dump,並在大量不相關的信息中挖掘問題。

    在本教程中,我們將看到如何過濾數據以有效診斷性能問題。此外,我們還將學習檢測瓶頸甚至簡單的錯誤。

    2. JVM中的線程

    JVM使用線程來執行每個內部和外部操作。眾所周知,垃圾回收進程具有自己的線程,而且Java應用程序內部的任務也創建自己的線程。

    在其生命週期中,線程會經歷各種狀態。每個線程都有一個跟踪當前操作的執行堆棧。此外,JVM還存儲成功調用的所有先前方法。因此,可以分析整個堆棧以研究出現問題時應用程序發生了什麼。

    為了展示本教程的主題,我們以一個簡單的Sender-Receiver應用程序( NetworkDriver )為例。 Java程序發送和接收數據包,因此我們將能夠分析幕後發生的事情。

    2.1。捕獲Java線程Dump

    應用程序運行後,可以通過多種方式生成用於診斷的Java線程轉儲。在本教程中,我們將使用JDK7 +安裝中包含的兩個實用程序。首先,我們將執行JVM Process Status(jps)命令來發現我們應用程序的PID進程:

    $ jps
    
     80661 NetworkDriver
    
     33751 Launcher
    
     80665 Jps
    
     80664 Launcher
    
     57113 Application
    

    其次,我們獲得應用程序的PID,在本例中為NetworkDriver.旁邊的PID NetworkDriver.然後,我們將使用jstack捕獲線程轉儲。最後,我們將結果存儲在文本文件中:

    $ jstack -l 80661 > sender-receiver-thread-dump.txt

    2.2。樣本轉儲的結構

    讓我們看一下生成的線程轉儲。第一行顯示時間戳,第二行顯示有關JVM的信息:

    2021-01-04 12:59:29
    
     Full thread dump OpenJDK 64-Bit Server VM (15.0.1+9-18 mixed mode, sharing):

    下一部分顯示了安全內存回收(SMR)和非JVM內部線程:

    Threads class SMR info:
    
     _java_thread_list=0x00007fd7a7a12cd0, length=13, elements={
    
     0x00007fd7aa808200, 0x00007fd7a7012c00, 0x00007fd7aa809800, 0x00007fd7a6009200,
    
     0x00007fd7ac008200, 0x00007fd7a6830c00, 0x00007fd7ab00a400, 0x00007fd7aa847800,
    
     0x00007fd7a6896200, 0x00007fd7a60c6800, 0x00007fd7a8858c00, 0x00007fd7ad054c00,
    
     0x00007fd7a7018800
    
     }

    然後,轉儲顯示線程列表。每個線程包含以下信息:

    • 名稱:如果開發人員包含有意義的線程名稱,它可以提供有用的信息
    • 優先級(prior):線程的優先級
    • Java ID (tid):JVM給定的唯一ID
    • 本機ID (nid):操作系統提供的唯一ID,可用於提取與CPU或內存處理的相關性
    • 狀態State:線程的實際狀態
    • 堆棧跟踪:最重要的信息源,可用來解釋我們的應用程序正在發生的情況

    我們可以從上到下看到快照時不同線程在做什麼。讓我們僅關注等待消息使用的堆棧中有趣的部分:

    "Monitor Ctrl-Break" #12 daemon prio=5 os_prio=31 cpu=17.42ms elapsed=11.42s tid=0x00007fd7a6896200 nid=0x6603 runnable [0x000070000dcc5000]
    
     java.lang.Thread.State: RUNNABLE
    
    
     at sun.nio.ch.SocketDispatcher.read0([email protected]/Native Method)
    
    
     at sun.nio.ch.SocketDispatcher.read([email protected]/SocketDispatcher.java:47)
    
    
     at sun.nio.ch.NioSocketImpl.tryRead([email protected]/NioSocketImpl.java:261)
    
    
     at sun.nio.ch.NioSocketImpl.implRead([email protected]/NioSocketImpl.java:312)
    
    
     at sun.nio.ch.NioSocketImpl.read([email protected]/NioSocketImpl.java:350)
    
    
     at sun.nio.ch.NioSocketImpl$1.read([email protected]/NioSocketImpl.java:803)
    
    
     at java.net.Socket$SocketInputStream.read([email protected]/Socket.java:981)
    
    
     at sun.nio.cs.StreamDecoder.readBytes([email protected]/StreamDecoder.java:297)
    
    
     at sun.nio.cs.StreamDecoder.implRead([email protected]/StreamDecoder.java:339)
    
    
     at sun.nio.cs.StreamDecoder.read([email protected]/StreamDecoder.java:188)
    
    
     - locked <0x000000070fc949b0> (a java.io.InputStreamReader)
    
    
     at java.io.InputStreamReader.read([email protected]/InputStreamReader.java:181)
    
    
     at java.io.BufferedReader.fill([email protected]/BufferedReader.java:161)
    
    
     at java.io.BufferedReader.readLine([email protected]/BufferedReader.java:326)
    
    
     - locked <0x000000070fc949b0> (a java.io.InputStreamReader)
    
    
     at java.io.BufferedReader.readLine([email protected]/BufferedReader.java:392)
    
    
     at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)
    
    
    
     Locked ownable synchronizers:
    
    
     - <0x000000070fc8a668> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

    乍一看,我們看到主堆棧跟踪正在執行java.io.BufferedReader.readLine ,這是預期的行為。如果進一步往下看,我們將在後台看到應用程序執行的所有JVM方法。因此,我們可以通過查看源代碼或其他內部JVM處理來確定問題的根源。

    在dump結束位置,我們會注意到還有一些其他線程**在執行後台操作,例如垃圾回收(GC)或對象**終止

    "VM Thread" os_prio=31 cpu=1.85ms elapsed=11.50s tid=0x00007fd7a7a0c170 nid=0x3603 runnable
    
     "GC Thread#0" os_prio=31 cpu=0.21ms elapsed=11.51s tid=0x00007fd7a5d12990 nid=0x4d03 runnable
    
     "G1 Main Marker" os_prio=31 cpu=0.06ms elapsed=11.51s tid=0x00007fd7a7a04a90 nid=0x3103 runnable
    
     "G1 Conc#0" os_prio=31 cpu=0.05ms elapsed=11.51s tid=0x00007fd7a5c10040 nid=0x3303 runnable
    
     "G1 Refine#0" os_prio=31 cpu=0.06ms elapsed=11.50s tid=0x00007fd7a5c2d080 nid=0x3403 runnable
    
     "G1 Young RemSet Sampling" os_prio=31 cpu=1.23ms elapsed=11.50s tid=0x00007fd7a9804220 nid=0x4603 runnable
    
     "VM Periodic Task Thread" os_prio=31 cpu=5.82ms elapsed=11.42s tid=0x00007fd7a5c35fd0 nid=0x9903 waiting on condition

    最後,dump顯示Java本機接口(JNI)引用。當發生內存洩漏時,我們應該特別注意這一點,因為它們不會自動垃圾回收:

    JNI global refs: 15, weak refs: 0

    所有的線程dump的結構非常相似,但是我們希望擺脫為用例生成的非重要數據。另一方面,我們需要對堆棧跟踪產生的大量日誌中的重要信息進行保存和分組。讓我們來看看如何做!

    3.分析線程轉儲的建議

    為了了解我們的應用程序正在發生什麼,我們需要有效地分析生成的快照。轉儲時,我們將獲得很多信息**和所有線程的精確數據**。但是,我們需要整理日誌文件,進行一些過濾和分組,以從堆棧跟踪中提取有用的提示。準備好轉儲後,我們將能夠使用其他工具來分析問題。讓我們看看如何解密樣本轉儲的內容。

    3.1。同步問題

    過濾掉堆棧跟踪的一個有趣的技巧是線程的狀態。我們將主要關注**RUNNABLE或BLOCKED線程,最後是TIMED_WAITING線程**。這些狀態將使我們指向兩個或多個線程之間發生衝突的方向:

    • 死鎖**情況下,多個正在運行的線程在共享對像上保留一個同步塊**
    • 線程爭用中當某個**線程被阻塞以等待其他線程完成時。**例如,上一節中生成的轉儲

    3.2。執行問題

    根據經驗,對於異常高的CPU使用率,我們只需要查看RUNNABLE線程即可。我們將線程轉儲與其他命令一起使用以獲取更多信息。這些命令之一是top -H -p PID,它顯示特定進程中哪些線程正在消耗OS資源。為了以防萬一,我們還需要查看內部JVM線程(例如GC)。另一方面,當處理性能異常低時我們將看一下BLOCKED線程。

    在那種情況下,一個轉儲肯定會不足以了解正在發生的事情。為了比較不同時間的相同線程的堆棧,我們需要**在很**短**的間隔內進行大量轉儲**。一方面,一張快照並不總是足以找出問題的根源。另一方面,我們需要避免快照之間的干擾(信息過多)。

    為了了解線程隨時間的演變,建議的最佳實踐是**至少進行3次dumps,每10秒進行一次**。另一個有用的技巧是將轉儲分成小塊,以避免崩潰加載文件。

    3.3。推薦建議

    為了有效地理解問題的根源,我們需要在堆棧跟踪中組織大量信息。因此,我們將考慮以下建議:

    • 在執行問題中,以10秒的間隔捕獲幾個快照將有助於重點解決實際問題。還建議根據需要拆分文件,以避免加載崩潰
    • 創建新線程時使用命名以更好地識別源代碼
    • 根據問題,忽略內部JVM處理(例如GC)
    • 發出異常的CPU或內存使用情況時,請關注**長時間運行或阻塞的線程**
    • 通過使用top -H -p PID將線程的堆棧與CPU處理相關聯
    • 最重要的是,使用分析器工具

    手動分析Java線程轉儲可能是一件乏味的工作。對於簡單的應用程序,可以識別產生問題的線程。另一方面,對於復雜的情況,我們需要工具來簡化此任務。在下一部分中,我們將使用為示例線程爭用生成的轉儲,展示如何使用這些工具。

    4.在線工具

    有幾種在線工具可用。使用此類軟件時,我們需要考慮安全性問題。請記住,我們可能會與第三方實體共享日誌

    4.1。快速線程

    FastThread可能是分析生產環境中的線程轉儲的最佳聯機工具。它提供了一個非常漂亮的圖形用戶界面。它還包括多種功能,例如線程的CPU使用率,堆棧長度以及最常用和最複雜的方法:

    如何分析Java線程轉儲

    FastThread集成了REST API功能以自動分析線程轉儲。使用簡單的cURL命令,可以立即發送結果。主要缺點是安全性,因為它**會將堆棧跟踪存儲在雲中**。

    4.2。 JStack評論

    JStack Review是一個在線工具,可以分析瀏覽器中的轉儲。它僅在客戶端,因此沒有數據存儲在計算機外部。從安全角度來看,這是使用它的主要優勢。它提供了所有線程的圖形概述,顯示了正在運行的方法,還按狀態將它們分組。 JStack Review將產生堆棧的線程與其餘線程分開,這對於忽略內部進程非常重要。最後,它還包括同步器和被忽略的行:

    如何分析Java線程轉儲

    4.3。 Spotify在線Java線程轉儲分析器

    Spotify在線Java線程轉儲分析器是一個用JavaScript編寫的在線開源工具。它以純文本格式顯示結果,該結果以帶和不帶堆棧的方式分隔線程。它還顯示正在運行的線程中的頂級方法:

    如何分析Java線程轉儲

    5.獨立應用程序

    我們也可以在本地使用幾個獨立的應用程序。

    5.1。 JProfiler

    JProfiler是市場上最強大的工具,在Java開發人員社區中廣為人知。可以使用10天的試用許可證來測試功能。 JProfiler允許創建概要文件並將正在運行的應用程序附加到它們。它包括多種功能,可以在現場識別問題,例如CPU和內存使用情況以及數據庫分析。它還支持與IDE的集成:

    如何分析Java線程轉儲

    5.2。 IBM Java線程監視器和轉儲分析器(TMDA)

    IBM TMDA可用於識別線程爭用,死鎖和瓶頸。它是免費分發和維護的,但不提供IBM的任何保證或支持:

    如何分析Java線程轉儲

    5.3。 Irockel**線程轉儲分析器(TDA)**

    Irockel TDA是LGPL v2.1許可的獨立開源工具。最新版本(v2.4)於2020年8月發布,因此維護良好。它將線程轉儲顯示為樹,還提供了一些統計信息以簡化導航:

    如何分析Java線程轉儲

    最後,IDE支持線程轉儲的基本分析,因此可以在開發期間調試應用程序。

    5.結論

    在本文中,我們演示了Java線程轉儲分析如何幫助我們查明同步或執行問題。

    最重要的是,我們回顧瞭如何正確分析它們,包括對組織快照中嵌入的大量信息的建議。