經過 Java 線程堆棧進行性能瓶頸分析

改善性能意味着用更少的資源作更多的事情。爲了利用併發來提升系統性能,咱們須要更有效的利用現有的處理器資源,這意味着咱們指望使 CPU 儘量出於忙碌狀態(固然,並非讓 CPU 週期出於應付無用計算,而是讓 CPU 作有用的事情而忙)。若是程序受限於當前的 CPU 計算能力,那麼咱們經過增長更多的處理器或者經過集羣就能提升總的性能。總的來講,性能提升,須要且僅須要解決當前的受限資源,當前受限資源多是:java

  • CPU: 若是當前 CPU 已經可以接近 100% 的利用率,而且代碼業務邏輯沒法再簡化,那麼說明該系統的性能以及達到上線,只有經過增長處理器來提升性能
  • 其餘資源:好比鏈接數等。能夠修改代碼,儘可能利用 CPU,能夠得到極大的性能提高

若是你的系統有以下的特色,說明系統存在性能瓶頸:算法

  • 隨着系統逐步增長壓力,CPU 使用率沒法趨近 100%(以下圖)數據庫

    image1.png
  • 持續運行緩慢。時常發現應用程序運行緩慢。經過改變環境因子(負載,鏈接數等)也沒法有效提高總體響應時間網絡

  • 系統性能隨時間的增長逐漸降低。在負載穩定的狀況下,系統運行時間越長速度越慢。多是因爲超出某個閾值範圍,系統運行頻繁出錯從而致使系統死鎖或崩潰
  • 系統性能隨負載的增長而逐漸降低。

一個好的程序,應該是可以充分利用 CPU 的。若是一個程序在單 CPU 的機器上不管多大壓力都不能使 CPU 使用率接近 100%,說明這個程序設計有問題。一個系統的性能瓶頸分析過程大體以下:多線程

  1. 先進性單流程的性能瓶頸分析,受限讓單流程的性能達到最優。
  2. 進行總體性能瓶頸分析。由於單流程性能最優,不必定整個系統性能最優。在多線程場合下,鎖爭用㩐給也會致使性能降低。

高性能在不一樣的應用場合下,有不一樣的含義:併發

  1. 有的場合高性能意味着用戶速度的體驗,如界面操做等
  2. 有的場合,高吞吐量意味着高性能,如短信或者彩信,系統更看重吞吐量,而對每個消息的處理時間不敏感
  3. 有的場合,是兩者的結合

性能調優的終極目標是:系統的 CPU 利用率接近 100%,若是 CPU 沒有被充分利用,那麼有以下幾個可能:oracle

  1. 施加的壓力不足
  2. 系統存在瓶頸

1 常見的性能瓶頸

1.1 因爲不恰當的同步致使的資源爭用

1.1.1 不相關的兩個函數,公用了一個鎖,或者不一樣的共享變量共用了同一個鎖,無謂地製造出了資源爭用

下面是一種常見的錯誤app

兩個不相干的方法(沒有使用同一個共享變量),共用了 this 鎖,致使人爲的資源競爭上面的代碼將 synchronized 加在類的每個方法上面,違背了保護什麼鎖什麼的原則。對於無共享資源的方法,使用了同一個鎖,人爲形成了沒必要要的等待。Java 缺省提供了 this 鎖,這樣不少人喜歡直接在方法上使用 synchronized 加鎖,不少狀況下這樣作是不恰當的,若是不考慮清楚就這樣作,很容易形成鎖粒度過大:socket

  • 即便一個方法中的代碼也不是到處須要鎖保護的。若是整個方法使用了 synchronized,那麼極可能就把 synchronized 的做用域給人爲擴大了。在方法級別上加鎖,是一種粗獷的鎖使用習慣。

上面的代碼應該變成下面數據庫設計


這樣會致使當前線程佔用鎖的時間過長,其餘須要鎖的線程只能等待,最終致使性能受到極大影響1.1.2 鎖的粒度過大,對共享資源訪問完成後,沒有將後續的代碼放在synchronized 同步代碼塊以外


單 CPU 場合 將耗時操做拿到同步塊以外,有的狀況下能夠提高性能,有的場合則不能:上面的代碼,會致使一個線程長時間佔有鎖,而在這麼長的時間裏其餘線程只能等待,這種寫法在不一樣的場合下有不一樣的提高餘地:

    • 同步塊的耗時代碼是 CPU 密集型代碼(純 CPU 運算等),不存在磁盤 IO/網絡 IO 等低 CPU 消耗的代碼,這種狀況下,因爲 CPU 執行這段代碼是 100% 的使用率,所以縮小同步塊也不會帶來任何性能上的提高。可是,同時縮小同步塊也不會帶來性能上的降低
    • 同步塊中的耗時代碼屬於磁盤/網絡 IO等低 CPU 消耗的代碼,噹噹前線程正在執行不消耗 CPU 的代碼時,這時候 CPU 是空閒的,若是此時讓 CPU 忙起來,能夠帶來總體性能上的提高,因此在這種場景下,將耗時操做的代碼放在同步以外,確定是能夠提升整個性能的(?)
  • 多 CPU 場合 將耗時的操做拿到同步塊以外,老是能夠提高性能
    • 同步塊的耗時代碼是 CPU 密集型代碼(純 CPU 運算等),不存在磁盤 IO/網絡 IO 等低 CPU 消耗的代碼,這種狀況下,因爲是多 CPU,其餘 CPU也許是空閒的,所以縮小同步塊可讓其餘線程立刻獲得執行這段代碼,能夠帶來性能的提高
    • 同步塊中的耗時代碼屬於磁盤/網絡 IO等低 CPU 消耗的代碼,噹噹前線程正在執行不消耗 CPU 的代碼時,這時候總有 CPU 是空閒的,若是此時讓 CPU 忙起來,能夠帶來總體性能上的提高,因此在這種場景下,將耗時操做的代碼放在同步塊以外,確定是能夠提升整個性能的

無論如何,縮小同步範圍,對系統沒有任何很差的影響,大多數狀況下,會帶來性能的提高,因此必定要縮小同步範圍,所以上面的代碼應該改成


Sleep 的濫用,尤爲是輪詢中使用 sleep,會讓用戶明顯感受到延遲,能夠修改成 notify 和 wait1.1.3 其餘問題

  • String + 的濫用,每次 + 都會產生一個臨時對象,並有數據的拷貝
  • 不恰當的線程模型
  • 效率地下的 SQL 語句或者不恰當的數據庫設計
  • 不恰當的 GC 參數設置致使的性能低下
  • 線程數量不足
  • 內存泄漏致使的頻繁 GC

2.2 性能瓶頸分析的手段和工具

上面提到的這些緣由造成的性能瓶頸,均可以經過線程堆棧分析,找到根本緣由。

2.2.1 如何去模擬,發現性能瓶頸

性能瓶頸的幾個特徵:

  • 當前的性能瓶頸只有一處,只有當解決了這一處,才知道下一處。沒有解決當前性能瓶頸,下一處性能瓶頸是不會出現的。以下圖所示,第二段是瓶頸,解決第二段的瓶頸後,第一段就變成了瓶頸,如此反覆找到全部的性能瓶頸

image2.png

  • 性能瓶頸是動態的,低負載下不是瓶頸的地方,高負載下可能成爲瓶頸。因爲 JProfile 等性能剖析工具依附在 JVM 上帶來的開銷,使系統根本就沒法達到該瓶頸出現時須要的性能,所以在這種場景下線程堆棧分析纔是一個真正有效的方法

鑑於性能瓶頸的以上特色,進行性能模擬的時候,必定要使用比系統當前稍高的壓力下進行模擬,不然性能瓶頸不會出現。具體步驟以下:

image3.png

2.2.2 如何經過線程堆棧識別性能瓶頸

經過線程堆棧,能夠很容易的識別多線程場合下高負載的時候纔會出現的性能瓶頸。一旦一個系統出現性能瓶頸,最重要的就是識別性能瓶頸,而後根據識別的性能瓶頸進行修改。通常多線程系統,先按照線程的功能進行歸類(組),把執行相同功能代碼的線程做爲一組進行分析。當使用堆棧進行分析的時候,以這一組線程進行統計學分析。若是一個線程池爲不一樣的功能代碼服務,那麼將整個線程池的線程做爲一組進行分析便可。

通常一個系統一旦出現性能瓶頸,從堆棧上分析,有以下三種最爲典型的堆棧特徵:

  1. 絕大多數線程的堆棧都表現爲在同一個調用上下文,且只剩下很是少的空閒線程。可能的緣由以下:
    • 線程的數量過少
    • 鎖的粒度過大致使的鎖競爭
    • 資源競爭
    • 鎖範圍中有大量耗時操做
    • 遠程通訊的對方處理緩慢
  2. 絕大多數線程出於等待狀態,只有幾個工做的線程,整體性能上不去。可能的緣由是,系統存在關鍵路徑,關鍵路徑已經達到瓶頸
  3. 線程總的數量不多(有些線程池的實現是按需建立線程,可能程序中建立線程

一個例子

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

"Thread-243" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable

[0xaeedb000..0xaeedc480]

at java.net.SocketInputStream.socketRead0(Native Method)

at java.net.SocketInputStream.read(SocketInputStream.java:129)

at oracle.net.ns.Packet.receive(Unknown Source)

... ...

at oracle.jdbc.driver.LongRawAccessor.getBytes()

at oracle.jdbc.driver.OracleResultSetImpl.getBytes()

- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)

at oracle.jdbc.driver.OracleResultSet.getBytes(O)

... ...

at org.hibernate.loader.hql.QueryLoader.list()

at org.hibernate.hql.ast.QueryTranslatorImpl.list()

... ...

at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)

at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)

at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)

- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)

at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)

at com.wes.threadpool.PooledExecutorEx$Worker.run()

at java.lang.Thread.run(Thread.java:595)

"Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable

[0xaeedb000..0xaeedc480]

at java.net.SocketInputStream.socketRead0(Native Method)

at java.net.SocketInputStream.read(SocketInputStream.java:129)

at oracle.net.ns.Packet.receive(Unknown Source)

... ...

at oracle.jdbc.driver.LongRawAccessor.getBytes()

at oracle.jdbc.driver.OracleResultSetImpl.getBytes()

- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)

at oracle.jdbc.driver.OracleResultSet.getBytes(O)

... ...

at org.hibernate.loader.hql.QueryLoader.list()

at org.hibernate.hql.ast.QueryTranslatorImpl.list()

... ...

at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)

at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)

at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)

- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)

at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)

at com.wes.threadpool.PooledExecutorEx$Worker.run()

at java.lang.Thread.run(Thread.java:595)

... ...

"Thread-238" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()

[0xaec56000..0xaec57700]

at java.lang.Object.wait(Native Method)

at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)

- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)

at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)

... ...

at org.hibernate.impl.SessionImpl.list()

at org.hibernate.impl.SessionImpl.find()

at com.wes.DBSessionMediatorImpl.find()

at com.wes.ResourceDBInteractorImpl.getCallBackObj()

at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)

at com.wes.timer.TimerTaskImpl.executeAll()

at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)

- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)

at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)

at com.wes.threadpool.PooledExecutorEx$Worker.run()

at java.lang.Thread.run(Thread.java:595)

 

 

"Thread-233" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()

[0xaec56000..0xaec57700]

 

at java.lang.Object.wait(Native Method)

at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)

- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)

at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)

... ...

at org.hibernate.impl.SessionImpl.list()

at org.hibernate.impl.SessionImpl.find()

at com.wes.DBSessionMediatorImpl.find()

at com.wes.ResourceDBInteractorImpl.getCallBackObj()

at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)

at com.wes.timer.TimerTaskImpl.executeAll()

at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)

- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)

at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)

at com.wes.threadpool.PooledExecutorEx$Worker.run()

at java.lang.Thread.run(Thread.java:595)

... ...

從堆棧看,有 51 個(socket)訪問,其中有 50 個是 JDBC 數據庫訪問。其餘方法被阻塞在 java.lang.Object.wait() 方法上。

2.2.3 其餘提升性能的方法

減小鎖的粒度,好比 ConcurrentHashMap 的實現默認使用 16 個鎖的 Array(有一個反作用:鎖整個容器會很費力,能夠添加一個全局鎖)

2.2.4 性能調優的終結條件

性能調優總有一個終止條件,若是系統知足以下兩個條件,便可終止:

  1. 算法足夠優化
  2. 沒有線程/資源的使用不當而致使的 CPU 利用不足
相關文章
相關標籤/搜索