通过 Java 线程堆栈进行性能瓶颈分析

释放双眼,带上耳机,听听看~!

改善性能意味着用更少的资源做更多的事情。为了利用并发来提高系统性能,我们需要更有效的利用现有的处理器资源,这意味着我们期望使CPU尽可能出于忙碌状态(当然,并不是让CPU周期出来应付无用计算,而是让CPU做有用的事情而忙)。如果程序受限于当前的CPU计算能力,那么我们通过增加更多的处理器或者通过集群就能提高总的性能总的来说,性能提高,需要且仅需要解决当前的受限资源,当前受限资源可能是:

  • CPU:如果当前CPU已经能够接近100%的利用率,并且代码业务逻辑无法再简化,那么说明该系统的性能以及达到上线,只有通过增加处理器来提高性能
  • 其他资源:比如连接数等。可以修改代码,尽量利用CPU,可以获得极大的性能提升

如果你的系统有如下的特点,说明系统存在性能瓶颈:

  • 随着系统逐步增加压力,CPU使用率无法趋近100%(如下图)

通过 Java 线程堆栈进行性能瓶颈分析

  • 持续运行缓慢。时常发现应用程序运行缓慢。通过改变环境因子(负载,连接数等)也无法有效提升整体响应时间

  • 系统性能随时间的增加逐渐下降。在负载稳定的情况下,系统运行时间越长速度越慢。可能是由于超出某个阈值范围,系统运行频繁出错从而导致系统死锁或崩溃

  • 系统性能随负载的增加而逐渐下降。

一个好的程序,应该是能够充分利用CPU的。如果一个程序在单CPU的机器上无论多大压力都不能使CPU使用率接近100%,说明这个程序设计有问题。一个系统的性能瓶颈分析过程大致如下:

  1. 先进性单流程的性能瓶颈分析,受限让单流程的性能达到最优。
  2. 进行整体性能瓶颈分析。因为单流程性能最优,不一定整个系统性能最优。在多线程场合下,锁争用㩐给也会导致性能下降。

高性能在不同的应用场合下,有不同的含义:

  1. 有的场合高性能意味着用户速度的体验,如界面操作等
  2. 有的场合,高吞吐量意味着高性能,如短信或者彩信,系统更看重吞吐量,而对每一个消息的处理时间不敏感
  3. 有的场合,是二者的结合

性能调优的终极目标是:系统的CPU利用率接近100%,如果CPU没有被充分利用,那么有如下几个可能:

  1. 施加的压力不足
  2. 系统存在瓶颈

1常见的性能瓶颈

1.1由于不恰当的同步导致的资源争用

1.1.1不相关的两个函数,公用了一个锁,或者不同的共享变量共用了同一个锁,无谓地制造出了资源争用

下面是一种常见的错误


1
2
3
4
5
6
7
8
9
10
1class MyClass {
2    Object sharedObj;
3   synchronized fun1() {...} //     访问共享变量 sharedObj
4   synchronized fun2() {...} //     访问共享变量 sharedObj
5   synchronized fun3() {...} //     不访问共享变量  sharedObj
6   synchronized fun4() {...} //     不访问共享变量  sharedObj
7   synchronized fun5() {...} //     不访问共享变量  sharedObj
8}
9
10

上面的代码将同步加在类的每个方法上面,违背了保护什么锁什么的原则。对于无共享资源的方法,使用了同一个锁,人为造成了不必要的等待.Java缺省提供了this锁,这样很多人喜欢直接在方法上使用synchronized加锁,很多情况下这样做是不恰当的,如果不考虑清楚就这样做,很容易造成锁粒度过大:

  • 两个不相干的方法(没有使用同一个共享变量),共用了这个锁,导致人为的资源竞争
  • 即使一个方法中的代码也不是处理需要锁保护的。如果整个方法使用了synchronized,那么很可能就把synchronized作用域给人为扩大了。在方法级别上加锁,是一种粗犷的锁使用习惯。

上面的代码应该变成下面


1
2
3
4
5
6
7
8
9
10
1class MyClass {
2   Object sharedObj;
3   synchronized fun1() {...} //     访问共享变量 sharedObj
4   synchronized fun2() {...} //     访问共享变量 sharedObj
5   fun3() {...} //     不访问共享变量  sharedObj
6   fun4() {...} //     不访问共享变量  sharedObj
7   fun5() {...} //     不访问共享变量  sharedObj
8}
9
10

1.1.2锁的粒度过大,对共享资源访问完成后,没有后续的代码放在synchronized同步代码块之外

这样会导致当前线程占用锁的时间过长,其他需要锁的线程只能等待,最终导致性能受到极大影响


1
2
3
4
5
6
7
8
9
1void fun1()
2{
3    synchronized(lock) {
4    ...... //正在访问共享资源
5    ...... //做其他耗时操作,但这些耗时操作与共享资源无关
6    }
7}
8
9

上面的代码,会导致一个线程长时间占有锁,而在这么长的时间里其他线程只能等待,这种写法在不同的场合下有不同的提升余地:

  • 单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忙起来,可以带来整体性能上的提升,所以在这种场景下,将耗时操作的代码放在同步块之外,肯定是可以提高整个性能的

不管如何,缩小同步范围,对系统没有任何不好的影响,大多数情况下,会带来性能的提升,所以一定要缩小同步范围,因此上面的代码应该改为


1
2
3
4
5
6
7
8
9
1void fun1()
2{
3     synchronized(lock) {
4       ...... //正在访问共享资源
5   }
6   ...... //做其他耗时操作,但这些耗时操作与共享资源无关
7}
8
9

1.1.3其他问题

  • Sleep的滥用,尤其是轮询中使用sleep,会让用户明显感觉到延迟,可以修改为notify and wait
  • String +的滥用,每次+都会产生一个临时对象,并有数据的拷贝
  • 不恰当的线程模型
  • 效率地下的SQL语句或者不恰当的数据库设计
  • 不恰当的GC参数设置导致的性能低下
  • 线程数量不足
  • 内存泄漏导致的频繁GC

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

上面提到的这些原因形成的性能瓶颈,都可以通过线程堆栈分析,找到根本原因。

2.2.1如何去模拟,发现性能瓶颈

性能瓶颈的几个特征:

  • 当前的性能瓶颈只有一处,只有当解决了这一处,才知道下一处。没有解决当前性能瓶颈,下一处性能瓶颈是不会出现的。如下图所示,第二段是瓶颈,解决第二段的瓶颈后,第一段就变成了瓶颈,如此反复找到所有的性能瓶颈

通过 Java 线程堆栈进行性能瓶颈分析

  • 性能瓶颈是动态的,低负载下不是瓶颈的地方,高负载下可能成为瓶颈。由于JProfile等性能剖析工具依附在JVM上带来的开销,使系统根本就无法达到该瓶颈出现时需要的性能,因此在这种场景下线程堆栈分析才是一个真正有效的方法

。鉴于性能瓶颈的以上特点,进行性能模拟的时候,一定要使用比系统当前稍高的压力下进行模拟,否则性能瓶颈不会出现具体步骤如下:

通过 Java 线程堆栈进行性能瓶颈分析

2.2.2如何通过线程堆栈识别性能瓶颈

通过线程堆栈,可以很容易的识别多线程场合下高负载的时候才会出现的性能瓶颈。一旦一个系统出现性能瓶颈,最重要的就是识别性能瓶颈,然后根据识别的性能瓶颈进行修改。一般多线程系统,先按照线程的功能进行归类(组),把执行相同功能代码的线程作为一组进行分析。当使用堆栈进行分析的时候,以这一组线程进行统计学分析。如果一个线程池为不同的功能代码服务,那么将整个线程池的线程作为一组进行分析即可。

一般一个系统一旦出现性能瓶颈,从堆栈上分析,有如下三种最为典型的堆栈特征:

针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然如果想学习Java的工程化,高性能及分布式,深入浅出微服务,春天,MyBatis的,Netty的源码分析的朋友可以加我的Java的进阶群:。725219329,群里有阿里大牛直播讲解技术,以及Java的大型互联网技术的视频免费分享给大家。

  1. 。绝大多数线程的堆栈都表现为在同一个调用上下文,且只剩下非常少的空闲线程可能的原因如下:
  • 线程的数量过少

    • 锁的粒度过大导致的锁竞争
    • 资源竞争
    • 锁范围中有大量耗时操作
    • 远程通信的对方处理缓慢
  • 绝大多数线程出于等待状态,只有几个工作的线程,总体性能上不去。可能的原因是,系统存在关键路径,关键路径已经达到瓶颈

  • 线程总的数量很少(有些线程池的实现是按需创建线程,可能程序中创建线程

一个例子


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
84
85
1"Thread-243" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
2   [0xaeedb000..0xaeedc480]
3          at java.net.SocketInputStream.socketRead0(Native Method)
4          at java.net.SocketInputStream.read(SocketInputStream.java:129)
5          at oracle.net.ns.Packet.receive(Unknown Source)
6          ... ...
7                 at oracle.jdbc.driver.LongRawAccessor.getBytes()
8          at oracle.jdbc.driver.OracleResultSetImpl.getBytes()
9          - locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)
10         at oracle.jdbc.driver.OracleResultSet.getBytes(O)
11         ... ...
12                at org.hibernate.loader.hql.QueryLoader.list()
13         at org.hibernate.hql.ast.QueryTranslatorImpl.list()
14         ... ...
15                at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)
16         at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)
17         at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
18         - locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)
19         at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
20         at com.wes.threadpool.PooledExecutorEx$Worker.run()
21         at java.lang.Thread.run(Thread.java:595)
22  "Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
23     [0xaeedb000..0xaeedc480]
24            at java.net.SocketInputStream.socketRead0(Native Method)
25         at java.net.SocketInputStream.read(SocketInputStream.java:129)
26         at oracle.net.ns.Packet.receive(Unknown Source)
27         ... ...
28                at oracle.jdbc.driver.LongRawAccessor.getBytes()
29         at oracle.jdbc.driver.OracleResultSetImpl.getBytes()
30         - locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)
31         at oracle.jdbc.driver.OracleResultSet.getBytes(O)
32         ... ...
33                at org.hibernate.loader.hql.QueryLoader.list()
34         at org.hibernate.hql.ast.QueryTranslatorImpl.list()
35         ... ...
36                 at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)
37          at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)
38          at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
39          - locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)
40          at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
41          at com.wes.threadpool.PooledExecutorEx$Worker.run()
42          at java.lang.Thread.run(Thread.java:595)
43          ... ...
44          "Thread-238" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()
45          [0xaec56000..0xaec57700]
46              at java.lang.Object.wait(Native Method)
47      at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)
48      - locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)
49      at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)
50      ... ...
51          at org.hibernate.impl.SessionImpl.list()
52      at org.hibernate.impl.SessionImpl.find()
53      at com.wes.DBSessionMediatorImpl.find()
54      at com.wes.ResourceDBInteractorImpl.getCallBackObj()
55      at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)
56      at com.wes.timer.TimerTaskImpl.executeAll()
57      at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
58      - locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)
59      at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
60      at com.wes.threadpool.PooledExecutorEx$Worker.run()
61      at java.lang.Thread.run(Thread.java:595)
62   
63   
64  "Thread-233" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()
65  [0xaec56000..0xaec57700]
66   
67      at java.lang.Object.wait(Native Method)
68      at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)
69      - locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)
70      at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)
71      ... ...
72          at org.hibernate.impl.SessionImpl.list()
73      at org.hibernate.impl.SessionImpl.find()
74      at com.wes.DBSessionMediatorImpl.find()
75      at com.wes.ResourceDBInteractorImpl.getCallBackObj()
76      at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)
77      at com.wes.timer.TimerTaskImpl.executeAll()
78      at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
79      - locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)
80      at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
81      at com.wes.threadpool.PooledExecutorEx$Worker.run()
82      at java.lang.Thread.run(Thread.java:595)
83      ... ...
84
85

从堆栈看,有51个(socket)访问,其中有50个是JDBC数据库访问。其他方法被阻塞在java.lang.Object.wait()方法上。

针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然如果想学习Java的工程化,高性能及分布式,深入浅出微服务,春天,MyBatis的,Netty的源码分析的朋友可以加我的Java的进阶群:。725219329,群里有阿里大牛直播讲解技术,以及Java的大型互联网技术的视频免费分享给大家。

2.2.3其他提高性能的方法

减少锁的粒度,比如ConcurrentHashMap的实现默认使用16个锁的Array(有一个副作用:锁整个容器会很费力,可以添加一个全局锁)

2.2.4性能调优的终结条件

性能调优总有一个终止条件,如果系统满足如下两个条件,即可终止:

  1. 算法足够优化
  2. 没有线程/资源的使用不当而导致的CPU利用不足

关注作者微信公众号:“Java的的的架构师学习”

一个专注分享架构技术的公众号

通过 Java 线程堆栈进行性能瓶颈分析

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索