Java性能分析之線程棧詳解與性能分析

Java性能分析之線程棧詳解

Java性能分析邁不過去的一個關鍵點是線程棧,新的性能班級也講到了JVM這一塊,因此本篇文章對線程棧進行基礎知識普及以及如何對線程棧進行性能分析。java

 

基本概念

 

線程堆棧也稱線程調用堆棧,是虛擬機中線程(包括鎖)狀態的一個瞬間狀態的快照,即系統在某一個時刻全部線程的運行狀態,包括每個線程的調用堆棧,鎖的持有狀況。雖然不一樣的虛擬機打印出來的格式有些不一樣,可是線程堆棧的信息都包含面試

一、線程名字,id,線程的數量等。apache

二、線程的運行狀態,鎖的狀態(鎖被哪一個線程持有,哪一個線程在等待鎖等)緩存

三、調用堆棧(即函數的調用層次關係)調用堆棧包含完整的類名,所執行的方法,源代碼的行數。tomcat

 

線程做用

 

由於線程棧是瞬時快照包含線程狀態以及調用關係,因此藉助堆棧信息能夠幫助分析不少問題,好比線程死鎖,鎖爭用,死循環,識別耗時操做等等。線程棧是瞬時記錄,因此沒有歷史消息的回溯,通常咱們都須要結合程序的日誌進行跟蹤,通常線程棧能分析以下性能問題:服務器

一、系統平白無故的cpu太高網絡

二、系統掛起,無響應異步

三、系統運行愈來愈慢socket

四、性能瓶頸(如沒法充分利用cpu等)函數

五、線程死鎖,死循環等

六、因爲線程數量太多致使的內存溢出(如沒法建立線程等)

 

線程棧狀態

 

線程棧狀態有以下幾種

一、NEW

二、RUNNABLE

三、BLOCKED

四、WAITING

五、TIMED_WAITING

六、TERMINATED

下面依次對6種線程棧狀態進行介紹。

 

線程棧狀態詳解

 

一、NEW

線程剛剛被建立,也就是已經new過了,可是尚未調用start()方法,這個狀態咱們使用jstack進行線程棧dump的時候基本看不到,由於是線程剛建立時候的狀態。

二、RUNNABLE

從虛擬機的角度看,線程正在運行狀態,狀態是線程正在正常運行中, 固然可能會有某種耗時計算/IO等待的操做/CPU時間片切換等, 這個狀態下發生的等待通常是其餘系統資源, 而不是鎖, Sleep等

處於RUNNABLE狀態的線程是否是必定會消耗cpu呢,不必定,像socket IO操做,線程正在從網絡上讀取數據,儘管線程狀態RUNNABLE,但實際上網絡io,線程絕大多數時間是被掛起的,只有當數據到達後,線程纔會被喚起,掛起發生在本地代碼(native)中,虛擬機根本不一致,不像顯式的調用sleep和wait方法,虛擬機才能知道線程的真正狀態,但在本地代碼中的掛起,虛擬機沒法知道真正的線程狀態,所以一律顯示爲RUNNABLE。

 

三、BLOCKED

線程處於阻塞狀態,正在等待一個monitor lock。一般狀況下,是由於本線程與其餘線程公用了一個鎖。其餘在線程正在使用這個鎖進入某個synchronized同步方法塊或者方法,而本線程進入這個同步代碼塊也須要這個鎖,最終致使本線程處於阻塞狀態。

 

真實生活例子:

 

今天你要去阿里面試。這是你夢想的工做,你已經盯着它多年了。你早上起來,準備好,穿上你最好的外衣,對着鏡子打理好。當你走進車庫發現你的朋友已經把車開走了。在這個場景,你只有一輛車,因此怎麼辦?在真實生活中,可能會打架搶車。 如今由於你朋友把車開走了你被BLOCKED了。你不能去參加面試。

這就是BLOCKED狀態。用技術術語講,你是線程T1,你朋友是線程T2,而鎖是車。T1BLOCKED在鎖(例子裏的車)上,由於T2已經獲取了這個鎖。

 

四、WAITING

這個狀態下是指線程擁有了某個鎖以後, 調用了他的wait方法, 等待其餘線程/鎖擁有者調用 notify / notifyAll一遍該線程能夠繼續下一步操做, 這裏要區分 BLOCKED 和 WATING 的區別, 一個是在臨界點外面等待進入, 一個是在理解點裏面wait等待別人notify, 線程調用了join方法 join了另外的線程的時候, 也會進入WAITING狀態, 等待被他join的線程執行結束,處於waiting狀態的線程基本不消耗CPU。

 

真實生活例子:

 

再看下幾分鐘後你的朋友開車回家了,鎖(車)就被釋放了,如今你意識到快到面試時間了,而開車過去很遠。因此你拼命地踩油門。限速120KM/H而你以160KM/H的速度在開。很不幸,一個交警發現你超速了,讓你停到路邊。如今你進入了WAITING狀態。你停下車坐在那等着交警過來檢查開罰單而後給你放行。基本上,你只有等他讓你走(你無法開車逃),你被卡在WAITING狀態了。

用技術術語來說,你是線程T1而交警是線程T2。你釋放你的鎖(例子中你停下了車),並進入WAITING狀態,直到警察(例子中T2)讓你走,你陷入了WAITING狀態。

 

小貼士:當線程調用如下方法時會進入WAITING狀態:

一、Object#wait() 並且不加超時參數

二、Thread#join() 並且不加超時參數

三、LockSupport#park()

 

 

五、TIMED_WAITING

 

該線程正在等待,經過使用了 sleep, wait, join 或者是 park 方法。(這個與 WAITING 不一樣是經過方法參數指定了最大等待時間,WAITING 能夠經過時間或者是外部的變化解除),線程等待指定的時間。

真實生活例子:

 

儘管此次面試過程充滿戲劇性,但你在面試中作的很是好,驚豔了全部人並得到了高薪工做。你回家告訴你的鄰居你的新工做並表達你激動的心情。你的朋友告訴你他也在同一個辦公樓裏工做。他建議你坐他的車去上班。你想這不錯。因此去阿里上班的第一天,你走到你鄰居的房子,在他的房子前停好你的車。你等了他10分鐘,但你的鄰居沒有出現。你而後繼續開本身的車去上班,這樣你不會在第一天就遲到。這就是TIMED_WAITING.

 

用技術術語來解釋,你是線程T1而你的鄰居是線程T2。你釋放了鎖(這裏是中止開車)並等了足足10分鐘。若是你的鄰居T2沒有來,你繼續開車(老司機注意車速,其餘乘客記得買票)。

 

 

小貼士:調用瞭如下方法的線程會進入TIMED_WAITING

一、Thread#sleep()

二、Object#wait() 並加了超時參數

三、Thread#join() 並加了超時參數

四、LockSupport#parkNanos()

五、LockSupport#parkUntil()

 

TIMED_WAITING (parking)實例以下:

 

從圖中能夠看出

1)「TIMED_WAITING (parking)」中的 timed_waiting 指等待狀態,但這裏指定了時間,到達指定的時間後自動退出等待狀態;parking指線程處於掛起中。

2)「waiting on condition」須要與堆棧中的「parking to wait for  <0x00000000acd84de8> (a java.util.concurrent.SynchronousQueue$TransferStack)」結合來看。

首先,本線程確定是在等待某個條件的發生,來把本身喚醒。

其次,SynchronousQueue 並非一個隊列,只是線程之間移交信息的機制,當咱們把一個元素放入到 SynchronousQueue 中時必須有另外一個線程正在等待接受移交的任務,所以這就是本線程在等待的條件。

3)別的就看不出來了。

 

 

TIMED_WAITING (on object monitor)狀態以下圖,表示當前線程被掛起一段時間,說明該線程正在執行obj.waiting time方法,該狀態的線程不消耗cpu。

從圖中能夠看出

1)「TIMED_WAITING (on object monitor)」,對於本例而言,是由於本線程調用了 java.lang.Object.wait(long timeout) 而進入等待狀態。

2)「Wait Set」中等待的線程狀態就是「 in Object.wait() 」。當線程得到了 Monitor,進入了臨界區以後,若是發現線程繼續運行的條件沒有知足,它則調用對象(通常就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 「Wait Set」隊列。只有當別的線程在該對象上調用了 notify() 或者 notifyAll() ,「 Wait Set」隊列中線程才獲得機會去競爭,可是隻有一個線程得到對象的 Monitor,恢復到運行態。

六、TERMINATED

線程終止,一樣咱們在使用jstack進行線程dump的時候也不多看到該狀態的線程棧。

 

狀態小結

 

這些狀態中NEW狀態是開始,TERMINATED是銷燬,在整個線程對象的運行過程當中,這個兩個狀態只能出現一次。其餘任何狀態均可以出現屢次,彼此之間能夠相互轉換

  • 處於timed_waiting/waiting狀態的線程必定不消耗cpu,處於runnable狀態的線程不必定會消耗cpu,要結合當前線程代碼的性質判斷,是否消耗cpu

  • 若是是純java運算代碼,則消耗cpu

  • 若是線程處於網絡io,不多消耗cpu

  • 若是是本地代碼,經過查看代碼,能夠經過pstack獲取本地的線程堆棧,若是是純運算代碼,則消耗cpu,若是被掛起,則不消耗,若是是io,則不怎麼消耗cpu。

如下是狀態轉化圖,能夠較爲清晰地看到狀態轉換的場景與條件:

線程棧解讀

從main線程看,線程堆棧裏面的最直觀的信息是當前線程的調用上下文,即從哪一個函數調用到哪一個函數(從下往上看),正執行到哪一類的哪一行,藉助這些信息,咱們就對當前系統正在作什麼一目瞭然。

 

 

其中"線程對應的本地線程Id號"所指的本地線程是指該java虛擬機所對應的虛擬機中的本地線程,咱們知道java是解析型語言,執行的實體是java虛擬機,所以java代碼是依附於java虛擬機的本地線程執行的,當啓動一個線程時,是建立一個native本地線程,本地線程纔是真實的線程實體,爲了更加深刻理解本地線程和java線程的關係,咱們能夠經過如下方式將java虛擬機的本地線程打印出來:

一、試用ps -ef|grep java 得到java進行id

二、試用pstack<java pid> 得到java虛擬機本地線程的堆棧

從操做系統打印出來的虛擬機的本地線程看,本地線程數量和java線程數量是相同的,說明兩者是一一對應的關係。

那麼本地線程號如何與java線程堆棧文件對應起來呢,每個線程都有tid,nid的屬性,經過這些屬性能夠對應相應的本地線程,咱們先看java線程第一行,裏面有一個屬性是nid,

main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]

其中nid是native thread id,也就是本地線程中的LWPID,兩者是相同的,只不過java線程中的nid用16進製表示,本地線程的id(top -H裏取到的java線程id)用十進制表示。3368的十六進制表示0xd28,在java線程堆棧中查找nid爲0xd28就是本地線程對應的java線程。

 

 

線程鎖解讀

 

線程棧中包含直接信息爲:線程個數,每一個線程調用的方法堆棧,當前鎖的狀態。從線程個數能夠直接數出來,線程調用的方法堆棧,從下向上看,表示了當前線程調用哪一個類哪一個方法,鎖的狀態看起來須要一些技巧,與鎖相關的重要信息以下:

 

  • 當一個線程佔有一個鎖的時候,線程堆棧會打印一個-locked<0x22bffb60>

  • 當一個線程正在等在其餘線程釋放該鎖,線程堆棧會打印一個-waiting to lock<0x22bffb60>

  • 當一個線程佔有一個鎖,但又執行在該鎖的wait上,線程堆棧中首先打印locked,而後打印-waiting on <0x22c03c60>

 

在線程堆棧中與鎖相關的三個最重要的特徵字:locked,waiting to lock,waiting on 瞭解這三個特徵字,就能夠對鎖進行分析了。

 

通常狀況下,當一個或一些線程正在等待一個鎖的時候,應該有一個線程佔用了這個鎖,即若是有一個線程正在等待一個鎖,該鎖必然被另外一個線程佔用,從線程堆棧中看,若是看到waiting to lock<0x22bffb60>,應該也應該有locked<0x22bffb60>,大多數狀況下確實如此,可是有些狀況下,會發現線程堆棧中可能根本沒有locked<0x22bffb60>,而只有waiting to ,這是什麼緣由呢,實際上,在一個線程釋放鎖和另外一個線程被喚醒之間有一個時間窗,若是這個期間,剛好打印堆棧信息,那麼只會找到waiting to ,可是找不到locked 該鎖的線程,固然不一樣的JAVA虛擬機有不一樣的實現策略,不必定會馬上響應請求,也許會等待正在執行的線程執行完成。

 

結合jstack結果對線程狀態詳解

上篇文章詳細介紹了線程棧的做用、狀態、任何查看理解,本篇文章結合jstack工具來查看線程狀態,並列出重點關注目標。Jstack是經常使用的排查工具,它能輸出在某一個時間,Java進程中全部線程的狀態,不少時候這些狀態信息能給咱們的排查工做帶來有用的線索。 Jstack的輸出中,Java線程狀態主要是如下幾種:

1、BLOCKED 線程在等待monitor鎖(synchronized關鍵字)

2、TIMED_WAITING 線程在等待喚醒,但設置了時限

3、WAITING 線程在無限等待喚醒

4、RUNNABLE 線程運行中或I/O等待

 

下面經過詳細的實例來對這幾種狀態進行解釋

BLOCKED

以下圖所示,爲使用jstack工具dump線程後,查看到的線程處於blocked狀態。dump線程後,最早看的是線程所處的狀態。這個線程處於Blocked狀態,咱們須要重點分析。

首先,咱們來逐條分析下jstack工具抓取到的線程信息:

jstack工具抓取到的線程信息,是從下往上分析的,由上圖可見,線程先是開始運行,以後運行業務的一些方法,直到調用 org.apache.log4j.Category.forcedLog以後,開始waiting to lock。

  • 線程的狀態是:BLOCKED (on object monitor)

  說明線程處於阻塞狀態,正在等待一個monitor lock。阻塞緣由是:由於本線程與其餘線程公用了一個鎖,這時,已經有其餘在線程正在使用這個鎖進入某個synchronized同步方法塊或者方法。當本線程想要進入這個同步代碼塊時,也須要這個鎖,但鎖已被佔用,從而致使本線程處於阻塞狀態。

  • 第一行中包含了線程名和id等信息,如上圖中的"druid-consumer-pool-3",nid(每一個線程都有線程pid,將該pid轉成16進制的值,即爲jstack結果中的nid,能夠經過nid惟一確認一個線程。)
  • 第一行中還有線程目前正在  waiting for monitor entry,仍是代表了線程在等待進入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()」。目前線程狀態爲:waiting for monitor entry,說明它是「Entry Set」裏面的線程。咱們稱被 synchronized保護起來的代碼段爲臨界區。當一個線程申請進入臨界區時,它就進入了 「Entry Set」隊列。

 

這時有兩種可能性:

 

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

 

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

 

在第一種狀況下,線程將處於 「Runnable」的狀態

 

而第二種狀況下,線程 DUMP會顯示處於 「waiting for monitor entry」

 

根據以上分析,咱們能夠看出,線程想要調用log4j,目的是打印日誌,可是因爲調用log4j寫日誌有鎖機制,因而線程被阻塞了。再排查項目使用的log4j版本,得知此版本存在性能bug,優化手段爲升級log4j版本或者調整日誌級別、優化日誌打印的內容,或者添加緩存。

 

  • waiting to lock <地址>

說明線程使用synchronized申請對象鎖未成功,因而開始等待別的線程釋放鎖。線程在監視器的進入區等待。這條通常在調用棧頂出現,線程狀態通常對應爲Blocked。

 

TIMED_WAITING

以下圖所示,爲使用jstack工具dump線程後,查看到的線程處於TIMED_WAITING狀態。

  • 線程的狀態是:TIMED_WAITING

  這時的線程處於sleep狀態,說明線程在有時限的等待另外一個線程的特定操做,通常會有超時時間喚醒。就通常狀況來講,出現TIMED_WAITING很正常,等待網絡IO等都會出現這種狀態,可是大量的線程處於TIMED_WAITING時,須要咱們重點分析。

  • 第一行中,顯示線程在waiting on condition,這說明線程在等待某個條件的發生,從而本身喚醒,或者是調用了 sleep(n)。

  當線程在waiting on condition時,線程狀態可能爲:

  一、java.lang.Thread.State: WAITING (parking):一直等某個條件發生;

  二、java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定時等待某個條件發生,即便這個條件不到來,也將定時喚醒本身。

  在咱們這個例子裏,線程處於 TIMED_WAITING狀態。

  • parking to wait for <地址>目標

  這裏即爲第一行「waiting on condition" 所等待的條件,等待是java.util.concurrent.CountDownLatch$Sync,這是一種閉鎖的實現,是一種同步工具類,能夠延遲線程的進度直到閉鎖到達終止狀態,其內部包含一個計數器,該計數器被初始化爲一個整數,表示須要等待事件的數量。由以上分析能夠知道,線程是由於向druid寫數據,因爲有同步機制,而進入TIMED_WAITING狀態

 

  • 和上個例子線程在parking to wait for 不一樣,在這個例子中,線程也是處於TIMED_WAITING狀態,可是第一行中顯示線程正在 in Object.wait(),第四行顯示線程waiting on <地址> 目標。

線程在in Object.wait(), 說明線程在得到了監視器以後,又調用了 java.lang.Object.wait() 方法。

上篇線程詳解(一)中說過等待monitor 的線程分爲兩種

在 「Entry Set」中等待的線程狀態是 「Waiting for monitor entry」

在 「Wait Set」中等待的線程狀態是 「in Object.wait()」

本例是在「Wait Set」中等待的線程,其狀態是in Object.wait(),這說明線程得到了 Monitor,可是線程繼續運行的條件沒有知足,則調用對象(通常就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 「Wait Set」隊列。

此時線程狀態大體爲如下幾種:

一、java.lang.Thread.State: TIMED_WAITING (on object monitor);

二、java.lang.Thread.State: WAITING (on object monitor);

本例中線程就處於TIMED_WAITING狀態。

 

WAITING

 以下圖所示,爲使用jstack工具dump線程後,查看到的線程處於WAITING狀態。

(1)線程的狀態是:WAITING

意思就是線程在等待另一個線程去解除它的等待狀態。一個典型的例子就是生產者消費者模型,當生產者生產太慢的時候,消費者要等待生產者生產才能去消費,這段時間消費者線程就處於waiting狀態。還可使用lock.wait()方法使線程進入waiting狀態,無超時的等待,必須等待lock.notify()或lock.notifyAll()或接收到interrupt信號才能退出等待狀態。

(2)parking to wait for <地址> 目標

第一行中,顯示線程在waiting on condition,這說明線程在等待某個條件的發生,從而本身喚醒。

當線程在waiting on condition時,線程狀態可能爲

java.lang.Thread.State: WAITING (parking):一直等某個條件發生;

java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定時等待某個條件發生,即便這個條件不到來,也將定時喚醒本身。

在這個例子裏,線程處於 WAITING狀態,parking to wait for所等待的是java.util.concurrent.locks.AbstractQueuedSynchronizer,這也是java實現同步機制。

 

RUNNABLE

以下圖所示,爲使用jstack工具dump線程後,查看到的線程處於RUNNABLE 狀態。

在這個例子裏,能夠清楚看到整個線程運行的過程。在線程運行過程當中,有不少次獲取鎖,即爲上圖中locked <地址> 目標,即此線程使用synchronized申請對象鎖成功,是監視器的擁有者,能夠在臨界區內進行操做。上圖所lock的內容有java IO的輸入輸出流等。

 

 

 

在一次測試過程當中,經過線程打印有了一個意外收穫

  以下面信息,「http-bio-18272-exec-258」,表示Tomcat 的啓動模式爲 bio模式,將bio模式改成nio模式,在該項目中,其餘條件不變,只將bio模式更改成nio模式,tps提高了一倍

 tomcat的運行模式有3種.修改他們的運行模式.3種模式的運行是否成功,能夠看他的啓動控制檯,或者啓動日誌.或者登陸他們的默認頁面http://localhost:8080/查看其中的服務器狀態。 

1)bio :默認的模式,性能很是低下,沒有通過任何優化處理和支持. 

2)nio :利用java的異步io護理技術,no blocking IO技術. 

想運行在該模式下,直接修改server.xml裏的Connector節點,修改protocol爲

<Connector port="80" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" URIEncoding="UTF-8" useBodyEncodingForURI="true" enableLookups="false" redirectPort="8443" />

 

 

啓動後,就能夠生效。 

3)apr 

安裝起來最困難,可是從操做系統級別來解決異步的IO問題,大幅度的提升性能. 

必需要安裝apr和native,直接啓動就支持apr。

相關文章
相關標籤/搜索