通过 Java 线程堆栈进行性能瓶颈分析
改善性能意味着用更少的资源做更多的事情。为了利用并发来提高系统性能,我们需要更有效的利用现有的处理器资源,这意味着我们期望使 CPU 尽可能出于忙碌状态(当然,并不是让 CPU 周期出于应付无用计算,而是让 CPU 做有用的事情而忙)。如果程序受限于当前的 CPU 计算能力,那么我们通过增加更多的处理器或者通过集群就能提高总的性能。总的来说,性能提高,需要且仅需要解决当前的受限资源,当前受限资源可能是:
- CPU: 如果当前 CPU 已经能够接近 100% 的利用率,并且代码业务逻辑无法再简化,那么说明该系统的性能以及达到上线,只有通过增加处理器来提高性能
- 其他资源:比如连接数等。可以修改代码,尽量利用 CPU,可以获得极大的性能提升
如果你的系统有如下的特点,说明系统存在性能瓶颈:
随着系统逐步增加压力,CPU 使用率无法趋近 100%(如下图)
持续运行缓慢。时常发现应用程序运行缓慢。通过改变环境因子(负载,连接数等)也无法有效提升整体响应时间
- 系统性能随时间的增加逐渐下降。在负载稳定的情况下,系统运行时间越长速度越慢。可能是由于超出某个阈值范围,系统运行频繁出错从而导致系统死锁或崩溃
- 系统性能随负载的增加而逐渐下降。
一个好的程序,应该是能够充分利用 CPU 的。如果一个程序在单 CPU 的机器上无论多大压力都不能使 CPU 使用率接近 100%,说明这个程序设计有问题。一个系统的性能瓶颈分析过程大致如下:
- 先进性单流程的性能瓶颈分析,受限让单流程的性能达到最优。
- 进行整体性能瓶颈分析。因为单流程性能最优,不一定整个系统性能最优。在多线程场合下,锁争用㩐给也会导致性能下降。
高性能在不同的应用场合下,有不同的含义:
- 有的场合高性能意味着用户速度的体验,如界面操作等
- 有的场合,高吞吐量意味着高性能,如短信或者彩信,系统更看重吞吐量,而对每一个消息的处理时间不敏感
- 有的场合,是二者的结合
性能调优的终极目标是:系统的 CPU 利用率接近 100%,如果 CPU 没有被充分利用,那么有如下几个可能:
- 施加的压力不足
- 系统存在瓶颈
1 常见的性能瓶颈
1.1 由于不恰当的同步导致的资源争用
1.1.1 不相关的两个函数,公用了一个锁,或者不同的共享变量共用了同一个锁,无谓地制造出了资源争用
下面是一种常见的错误
两个不相干的方法(没有使用同一个共享变量),共用了 this 锁,导致人为的资源竞争上面的代码将 synchronized 加在类的每一个方法上面,违背了保护什么锁什么的原则。对于无共享资源的方法,使用了同一个锁,人为造成了不必要的等待。Java 缺省提供了 this 锁,这样很多人喜欢直接在方法上使用 synchronized 加锁,很多情况下这样做是不恰当的,如果不考虑清楚就这样做,很容易造成锁粒度过大:
- 即使一个方法中的代码也不是处处需要锁保护的。如果整个方法使用了 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 如何去模拟,发现性能瓶颈
性能瓶颈的几个特征:
- 当前的性能瓶颈只有一处,只有当解决了这一处,才知道下一处。没有解决当前性能瓶颈,下一处性能瓶颈是不会出现的。如下图所示,第二段是瓶颈,解决第二段的瓶颈后,第一段就变成了瓶颈,如此反复找到所有的性能瓶颈
- 性能瓶颈是动态的,低负载下不是瓶颈的地方,高负载下可能成为瓶颈。由于 JProfile 等性能剖析工具依附在 JVM 上带来的开销,使系统根本就无法达到该瓶颈出现时需要的性能,因此在这种场景下线程堆栈分析才是一个真正有效的方法
鉴于性能瓶颈的以上特点,进行性能模拟的时候,一定要使用比系统当前稍高的压力下进行模拟,否则性能瓶颈不会出现。具体步骤如下:
2.2.2 如何通过线程堆栈识别性能瓶颈
通过线程堆栈,可以很容易的识别多线程场合下高负载的时候才会出现的性能瓶颈。一旦一个系统出现性能瓶颈,最重要的就是识别性能瓶颈,然后根据识别的性能瓶颈进行修改。一般多线程系统,先按照线程的功能进行归类(组),把执行相同功能代码的线程作为一组进行分析。当使用堆栈进行分析的时候,以这一组线程进行统计学分析。如果一个线程池为不同的功能代码服务,那么将整个线程池的线程作为一组进行分析即可。
一般一个系统一旦出现性能瓶颈,从堆栈上分析,有如下三种最为典型的堆栈特征:
- 绝大多数线程的堆栈都表现为在同一个调用上下文,且只剩下非常少的空闲线程。可能的原因如下:
- 线程的数量过少
- 锁的粒度过大导致的锁竞争
- 资源竞争
- 锁范围中有大量耗时操作
- 远程通信的对方处理缓慢
- 绝大多数线程出于等待状态,只有几个工作的线程,总体性能上不去。可能的原因是,系统存在关键路径,关键路径已经达到瓶颈
- 线程总的数量很少(有些线程池的实现是按需创建线程,可能程序中创建线程
一个例子
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 性能调优的终结条件
性能调优总有一个终止条件,如果系统满足如下两个条件,即可终止:
- 算法足够优化
- 没有线程/资源的使用不当而导致的 CPU 利用不足
通过 Java 线程堆栈进行性能瓶颈分析的更多相关文章
- 通过Java 线程堆栈进行性能瓶颈分析
改善性能意味着用更少的资源做更多的事情.为了利用并发来提高系统性能,我们需要更有效的利用现有的处理器资源,这意味着我们期望使 CPU 尽可能出于忙碌状态(当然,并不是让 CPU 周期出于应付无用计算, ...
- Java问题定位之Java线程堆栈分析
采用Java开发的大型应用系统越来越大,越来越复杂,很多系统集成在一起,整个系统看起来像个黑盒子.系统运行遭遇问题(系统停止响应,运行越来越慢,或者性能低下,甚至系统宕掉),如何速度命中问题的根本原因 ...
- Java线程堆栈分析
不知觉间工作已有一年了,闲下来的时候总会思考下,作为一名Java程序员,不能一直停留在开发业务使用框架上面.老话说得好,机会是留给有准备的人的,因此,开始计划看一些Java底层一点的东西,尝试开始在学 ...
- Java项目性能瓶颈分析及定位(八)——Java线程堆栈分析(五)
对于CPU而言,常见的瓶颈主要有两种:服务器的压力很小,但是CPU的利用率却很高,这样的性能瓶颈相对比较容易定位(好比我只是说了你一句,你就哭了,你的弱点立马就暴露出来了):给服务器施加的压力很大,但 ...
- Java问题定位之如何借助线程堆栈进行问题分析
在大型的应用中,线程堆栈打印出来特别多,如何从众多的信息中找到真正有用,有价值的信息,我们需要一定的技巧.本文对此详细介绍. 我们可以从三个方面分析:堆栈的局部信息,一次堆栈的统计信息,多个堆栈的对比 ...
- 怎样分析java线程堆栈日志
注: 该文章的原文是由 Tae Jin Gu 编写,原文地址为 How to Analyze Java Thread Dumps 当有障碍,或者是一个基于 JAVA 的 WEB 应用运行的比预期慢的时 ...
- Java线程池使用和分析(一)
线程池是可以控制线程创建.释放,并通过某种策略尝试复用线程去执行任务的一种管理框架,从而实现线程资源与任务之间的一种平衡. 以下分析基于 JDK1.7 以下是本文的目录大纲: 一.线程池架构 二.Th ...
- Java线程池使用和分析(二) - execute()原理
相关文章目录: Java线程池使用和分析(一) Java线程池使用和分析(二) - execute()原理 execute()是 java.util.concurrent.Executor接口中唯一的 ...
- Java 线程池(ThreadPoolExecutor)原理分析与使用
在我们的开发中"池"的概念并不罕见,有数据库连接池.线程池.对象池.常量池等等.下面我们主要针对线程池来一步一步揭开线程池的面纱. 使用线程池的好处 1.降低资源消耗 可以重复利用 ...
随机推荐
- NYOJ 会场安排问题
#include<iostream> #include<stdio.h> #include<string.h> #include<queue> #inc ...
- 列表 list 容器类型数据(str字符串, list列表, tuple元组, set集合, dict字典)--->元组 tuple-->字符串 str
# ### 列表 list 容器类型数据(str字符串, list列表, tuple元组, set集合, dict字典) # (1)定义一个列表 listvar = [] print(listvar, ...
- Centos7 下 yum -y install ntp 出现/var/run/yum.pid 已被锁定
[root@localhost ~ ]# yum -y install ntp已加载插件:fastestmirror, langpacksRepodata is over 2 weeks old. I ...
- abap中结构体嵌套结构体。
1: 结构体中嵌套结构体. *&---------------------------------------------------------------------* *& Re ...
- Hyperledger Fabric CA的命令行用法
介绍Hyperledger Fabric CA的命令行方式简单用法 Hyperledger Fabric CA由server和client两部分组成. 设置两个环境变量 export FABRIC_C ...
- 【Java】-NO.16.EBook.4.Java.1.007-【疯狂Java讲义第3版 李刚】- Java基础类
1.0.0 Summary Tittle:[Java]-NO.16.EBook.4.Java.1.007-[疯狂Java讲义第3版 李刚]- Java基础类 Style:EBook Series:J ...
- Elasticsearch6.13 升级6.24 单节点停机升级
Elasticsearch6.x 升级6.y 是支持滚动升级的,目前我们测试环境只有一个节点只能停机升级了 准备工作 禁用分片分配 curl -X PUT "localhost:9200/_ ...
- 我的Chrome插件
1.AdBlock 用来屏蔽广告,用过的人都说好. 2.Flash Block(Plus) 用来限制Flash的播放. 3.Flash Control 用来限制Flash的播放. 4.Full Pag ...
- apache分割数组和集合的分法
public static void main(String[] agrs){ String[] array={"1","2","", ...
- SQL Server 2014忘记SA密码或禁用而且Windows身份验证也无法登录的解决办法
SQL Server双重验证都无法验证的情况下如何处理 1.以管理员身份运行sql配置管理器 2.打开[Sql Server 服务]节点关掉所有服务 3.双击本地实例[SQL Server (MSSQ ...