天天看點

【JVM】jstack和dump線程分析(2)

一:jstack

jstack指令的文法格式: jstack  <pid>。可以用jps檢視java程序id。這裡要注意的是:

1. 不同的 JAVA虛機的線程 DUMP的建立方法和檔案格式是不一樣的,不同的 JVM版本, dump資訊也有差别。本文中,隻以 SUN的 hotspot JVM 5.0_06 為例。

2. 在實際運作中,往往一次 dump的資訊,還不足以确認問題。建議産生三次 dump資訊,如果每次 dump都指向同一個問題,我們才确定問題的典型性。

二:線程分析

2.1. JVM 線程

線上程中,有一些 JVM内部的背景線程,來執行譬如垃圾回收,或者低記憶體的檢測等等任務,這些線程往往在 JVM初始化的時候就存在,如下所示:

"Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000]  
        "CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798]  
        "Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000]  
        "Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8]  
  
        at java.lang.Object.wait(Native Method)  
  
        - waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)  
  
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)  
  
        - locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)  
  
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)  
  
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)  
  
        "Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38]  
  
        at java.lang.Object.wait(Native Method)  
  
        - waiting on <0xef600758> (a java.lang.ref.Reference$Lock)  
  
        at java.lang.Object.wait(Object.java:474)  
  
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)  
  
        - locked <0xef600758> (a java.lang.ref.Reference$Lock)  
  
        "VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable  
  
        "VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition
      

 我們能看到:

    * 線程的狀态: waiting on condition

    * 線程的調用棧

    * 線程的目前鎖住的資源: <0xef63d600>

2.2. 線程的狀态分析

       正如我們剛看到的那樣,線程的狀态是一個重要的名額,它會顯示線上程 Stacktrace的頭一行結尾的地方。那麼線程常見的有哪些狀态呢?線程在什麼樣的情況下會進入這種狀态呢?我們能從中發現什麼線索?< /span>

1.1 Runnable

該狀态表示線程具備所有運作條件,在運作隊列中準備作業系統的排程,或者正在運作。

1.2 Wait on condition

       該狀态出現線上程等待某個條件的發生。具體是什麼原因,可以結合 stacktrace來分析。最常見的情況是線程在等待網絡的讀寫,比如當網絡資料沒有準備好讀時,線程處于這種等待狀态,而一旦有資料準備好讀之後,線程會重新激活,讀取并處理資料。在 Java引入 NewIO之前,對于每個網絡連接配接,都有一個對應的線程來處理網絡的讀寫操作,即使沒有可讀寫的資料,線程仍然阻塞在讀寫操作上,這樣有可能造成資源浪費,而且給作業系統的線程排程也帶來壓力。在 NewIO裡采用了新的機制,編寫的伺服器程式的性能和可擴充性都得到提高。

        如果發現有大量的線程都在處在 Wait on condition,從線程 stack看, 正等待網絡讀寫,這可能是一個網絡瓶頸的征兆。因為網絡阻塞導緻線程無法執行。一種情況是網絡非常忙,幾 乎消耗了所有的帶寬,仍然有大量資料等待網絡讀 寫;另一種情況也可能是網絡空閑,但由于路由等問題,導緻包無法正常的到達。是以要結合系統的一些性能觀察工具來綜合分析,比如 netstat統計機關時間的發送包的數目,如果很明顯超過了所在網絡帶寬的限制 ; 觀察 cpu的使用率,如果系統态的 CPU時間,相對于使用者态的 CPU時間比例較高;如果程式運作在 Solaris 10平台上,可以用 dtrace工具看系統調用的情況,如果觀察到 read/write的系統調用的次數或者運作時間遙遙領先;這些都指向由于網絡帶寬所限導緻的網絡瓶頸。另外一種出現 Wait on condition的常見情況是該線程在 sleep,等待 sleep的時間到了時候,将被喚醒。

1.3 Waiting for monitor entry 和 in Object.wait()

         在多線程的 JAVA程式中,實作線程之間的同步,就要說說 Monitor。 Monitor是 Java中用以實作線程之間的互斥與協作的主要手段,它可以看成是對象或者 Class的鎖。每一個對象都有,也僅有一個 monitor。每個 Monitor在某個時刻,隻能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分别在兩個隊列 “ Entry Set”和 “Wait Set”裡面等候。在 “Entry Set”中等待的線程狀态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的線程狀态是 “in Object.wait()”。

        先看 “Entry Set”裡面的線程。我們稱被 synchronized保護起來的代碼段為臨界區。當一個線程申請進入臨界區時,它就進入了 “Entry Set”隊列。對應的 code就像:

synchronized(obj) {

.........

}

這時有兩種可能性:

     該 monitor不被其它線程擁有, Entry Set裡面也沒有其它等待線程。本線程即成為相應類或者對象的 Monitor的 Owner,執行臨界區的代碼

     該 monitor被其它線程擁有,本線程在 Entry Set隊列中等待。

在第一種情況下,線程将處于 “Runnable”的狀态,而第二種情況下,線程 DUMP會顯示處于 “waiting for monitor entry”。如下所示:

"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8]  
      
    at testthread.WaitThread.run(WaitThread.java:39)  
      
    - waiting to lock <0xef63bf08> (a java.lang.Object)  
      
    - locked <0xef63beb8> (a java.util.ArrayList)  
      
    at java.lang.Thread.run(Thread.java:595)  
           

  臨界區的設定,是為了保證其内部的代碼執行的原子性和完整性。但是因為臨界區在任何時間隻允許線程串行通過,這 和我們多線程的程式的初衷是相反的。 如果在多線程的程式中,大量使用 synchronized,或者不适當的使用了它,會造成大量線程在臨界區的入口等待,造成系統的性能大幅下降。如果線上程 DUMP中發現了這個情況,應該審查源碼,改程序式。

        現在我們再來看現線上程為什麼會進入 “Wait Set”。當線程獲得了 Monitor,進入了臨界區之後,如果發現線程繼續運作的條件沒有滿足,它則調用對象(一般就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 “Wait Set”隊列。隻有當别的線程在該對象上調用了 notify() 或者 notifyAll() , “ Wait Set”隊列中線程才得到機會去競争,但是隻有一個線程獲得對象的 Monitor,恢複到運作态。在 “Wait Set”中的線程, DUMP中表現為: in Object.wait(),類似于:

"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38]  
      
            at java.lang.Object.wait(Native Method)  
      
            - waiting on <0xef63beb8> (a java.util.ArrayList)  
      
            at java.lang.Object.wait(Object.java:474)  
      
            at testthread.MyWaitThread.run(MyWaitThread.java:40)  
      
            - locked <0xef63beb8> (a java.util.ArrayList)  
      
            at java.lang.Thread.run(Thread.java:595)        

仔細觀察上面的 DUMP資訊,你會發現它有以下兩行:

- locked <0xef63beb8> (a java.util.ArrayList)

- waiting on <0xef63beb8> (a java.util.ArrayList)

這裡需要解釋一下,為什麼先 lock了這個對象,然後又 waiting on同一個對象呢?讓我們看看這個線程對應的代碼:

synchronized(obj) {  
           .........  
           obj.wait();  
           .........  
    }         

線程的執行中,先用 synchronized 獲得了這個對象的 Monitor(對應于 locked <0xef63beb8> )。當執行到 obj.wait(), 線程即放棄了 Monitor的所有權,進入 “wait set”隊列(對應于 waiting on <0xef63beb8> )。

         往往在你的程式中,會出現多個類似的線程,他們都有相似的 DUMP資訊。這也可能是正常的。比如,在程式中,有多個服務線程,設計成從一個隊列裡面讀取請求資料。這個隊列就是 lock以及 waiting on的對象。當隊列為空的時候,這些線程都會在這個隊列上等待,直到隊列有了資料,這些線程被 Notify,當然隻有一個線程獲得了 lock,繼續執行,而其它線程繼續等待。

3. JDK 5.0 的 lock

        上面我們提到如果 synchronized和 monitor機制運用不當,可能會造成多線程程式的性能問題。在 JDK 5.0中,引入了 Lock機制,進而使開發者能更靈活的開發高性能的并發多線程程式,可以替代以往 JDK中的 synchronized和 Monitor的 機制。但是,要注意的是,因為 Lock類隻是一個普通類, JVM無從得知 Lock對象的占用情況,是以線上程 DUMP中,也不會包含關于 Lock的資訊, 關于死鎖等問題,就不如用 synchronized的程式設計方式容易識别。

4.案例分析

1.     死鎖

在多線程程式的編寫中,如果不适當的運用同步機制,則有可能造成程式的死鎖,經常表現為程式的停頓,或者不再響應使用者的請求。比如在下面這個示例中,是個較為典型的死鎖情況:

"Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000  
  
..0x02d3fd68]  
  
at deadlockthreads.TestThread.run(TestThread.java:31)  
  
- waiting to lock <0x22c19f18> (a java.lang.Object)  
  
- locked <0x22c19f20> (a java.lang.Object)  
  
"Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000  
  
..0x02cff9e8]  
  
at deadlockthreads.TestThread.run(TestThread.java:31)  
  
- waiting to lock <0x22c19f20> (a java.lang.Object)  
  
- locked <0x22c19f18> (a java.lang.Object)  
           

 在 JAVA 5中加強了對死鎖的檢測。線程 Dump中可以直接報告出 Java級别的死鎖,如下所示:

Found one Java-level deadlock:  
    =============================  
    "Thread-1":  
    waiting to lock monitor 0x0003f334 (object 0x22c19f18, a java.lang.Object),  
    which is held by "Thread-0"  
    "Thread-0":  
    waiting to lock monitor 0x0003f314 (object 0x22c19f20, a java.lang.Object),  
    which is held by "Thread-1"   
           

2.     熱鎖

        熱鎖,也往往是導緻系統性能瓶頸的主要因素。其表現特征為,由于多個線程對臨界區,或者鎖的競争,可能出現:

    * 頻繁的線程的上下文切換:從作業系統對線程的排程來看,當 線程在等待資源而阻塞的時候,作業系統會将之切換出來,放到等待的隊列,當線程獲得資源之後,排程算法會将這個線程切換進去,放到執行隊列中。

    * 大量的系統調用:因為線程的上下文切換,以及熱鎖的競争,或 者臨界區的頻繁的進出,都可能導緻大量的系統調用。

    * 大部分 CPU開銷用在 “系統态 ”:線程上下文切換,和系統調用,都會導緻 CPU在 “系統态 ”運作,換而言之,雖然系統很忙碌,但是 CPU用在 “使用者态 ”的比例較小,應用程式得不到充分的 CPU資源。

    * 随着 CPU數目的增多,系統的性能反而下降。因為 CPU數目多,同 時運作的線程就越多,可能就會造成更頻繁的線程上下文切換和系統态的 CPU開銷,進而導緻更糟糕的性能。

上面的描述,都是一個 scalability(可擴充性)很差的系統的表現。從整體的性能名額看,由于線程熱鎖的存在,程式的響應時間會變長,吞吐量會降低。< /span>

         那麼,怎麼去了解 “熱鎖 ”出現在什麼地方呢?一個重要的方法還是結合作業系統的各種工具觀察系統資源使用狀況,以及收集 Java線程的 DUMP資訊,看線程都阻塞在什麼方法上,了解原因,才能找到對應的解決方法。

        我們曾經遇到過這樣的例子,程式運作時,出現了以上指出的各種現象,通過觀察作業系統的資源使用統計資訊,以及線程 DUMP資訊,确定了程式中熱鎖的存在,并發現大多數的線程狀态都是 Waiting for monitor entry或者 Wait on monitor,且是阻塞在壓縮和解壓縮的方法上。後來采用第三方的壓縮包 javalib替代 JDK自帶的壓縮包後,系統的性能提高了幾倍。

轉載于:https://www.cnblogs.com/wentaos/p/7407367.html