经过 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 利用不足
相关文章
相关标签/搜索