如何利用线程堆栈定位问题

admin 2025-05-09 39人围观 ,发现200个评论
01背景

针对在一些服务中会出现的cpu飙高、死锁、线程假死等问题,总结和提炼排查问题的思路和解决方案非常重要。上述问题会涉及到线程堆栈,本文将结合实际案例来阐述一下线程堆栈的功能。

02基本知识2.1什么是线程堆栈

线程堆栈是系统当时某个时刻的线程运行状态(即瞬间快照)。

线程堆栈的信息包含

线程的名字、ID、线程的数量

线程的运行状态、锁的状态(锁被那个线程持有,哪个线程再等待锁)

调用堆栈(即函数的调用层次关系)。调用堆栈包含完整的类名,所执行的方法,源代码的行数

2.2线程堆栈能分析问题类型

线程堆栈定位问题只能定位在当前线程上留下痕迹的问题

2.3线程堆栈不能分析的问题

线程堆栈不能定位在线程堆栈上不留痕迹的问题:

并发bug导致的数据混乱,这种问题在线程堆栈中没有任何航迹,所以这种问题线程堆栈无法提供任何帮助。

数据库锁表的问题,表被锁,往往由于某个事务没有提交/回滚,但这些信息无法在堆栈中表现出现

在线程上不留痕迹的问题只能通过其他手段来进行定位。在实际的系统中,系统的问题分为几种类型:

在堆栈中能够表现出问题的,使用线程堆栈进行定位

无法在线程中留下痕迹的问题定位,需要依赖于一个好的日志设计

非常隐蔽的问题,只能依赖于丰富的代码经验,如多线程导致的数据混乱,以及后面提到的幽灵代码

2.4如何输出线程堆栈

kill-3pid命令只能打印那一瞬间java进程的堆栈信息,适合在服务器响应慢,cpu、内存快速飙升等异常情况下使用,可以方便地定位到导致异常发生的java类,解决如死锁、连接超时等原因导致的系统异常问题。该命令不会杀死进程。

备注:

Linux常用命令

命令

描述

ps

查找进程的pid

pstack

打印进程或者线程的栈信息

strace

统计每一步系统调用花费的时间

2.5分析线程堆栈-线程状态的分析2.5.1概述2.5.2Runnable

从虚拟机的角度来看,线程处于正在运行的状态。

那么处于RUNNABLE的线程是不是一定消耗CPU呢?实际上不一定。下面的线程堆栈表示该线程正在从网络读取数据,尽管下面这个线程显示为RUNNABLE状态,但实际上网络IO,线程绝大多数时间是被挂起,只有当数据到达之后,线程才被重新唤醒。挂起发生在本地代码(Native)中,虚拟机根本不知道,不像显式调用了Java的sleep()或者wait()等方法,虚拟机能知道线程的真正状态,但对于本地代码中的挂起,虚拟机无法真正地知道线程状态,因此它一概显示为RUNNABLE。像这种socketIO操作不会消耗大量的CPU,因为大多时间在等待,只有数据到来之后,才消耗一点点CPU.

Thread-39"daemonprio=1tid=0x08646590nid=0x666drunnable[5beb7000..5beb88b8]:(NativeMethod)(:129)(:183)(:201)-locked0x47bfb940()_(PG_:141)(:68)-locked0x47bfb758(_Stream)(:398)复制代码

下面的线程正在执行纯Java代码指令,实实在在是消耗CPU的线程。

"Thread-444"prio=1tid=0xa4853568nid=0x7aderunnable[0xafcf7000..0xafcf8680]:RUNNABLE//实实在在再对应CPU运算指令(UnknownSource)(UnknownSource)(:51)()()()(:164)()()复制代码

下面的线程正在进行JNI本地方法调用,具体是否消耗CPU,要看TcpRecvExt的实现,如果TcpRecvExt是纯运算代码,那么是实实在在消耗CPU,如果TcpRecvExt()中存在挂起的代码,那么该线程尽管显示为RUNNABLE,但实际上也是不消耗CPU的。

"ClientReceiveThread"daemonprio=1tid=0x99dbacf8nid=0x7988runnable[]:(NativeMethod)(:60)$(:333)(:94)复制代码
2.5.3TIMED_WAITING(onobjectmonitor)

表示当前线程被挂起一段时间,说明该线程正在执行(inttime)方法.

下面的线程堆栈表示当前线程正处于TIMED_WAITING状态,当前正在被挂起,时长为参数中指定的时长,如(2000)。因此该线程当前不消耗CPU。

"JMXserver"daemonprio=6tid=0x0ad2c800nid=0()[]:TIMED_WAITING(onobjectmonitor)(NativeMethod)-waitingon0x03129da0(a[I)$(:150)-locked0x03129da0(a[I)(:620)复制代码
2.5.4TIMED_WAITING(sleeping)

表示当前线程被挂起一段时间,即正在执行(inttime)方法.

下面的线程正处于TIMED_WAITING状态,表示当前被挂起一段时间,时长为参数中指定的时长,如(100000)。因此该线程当前不消耗CPU。

"Commthread"daemonprio=10tid=0x00002aaad4107400nid=0x649fwaitingoncondition[0x000000004133b000..0x000000004133ba00]:TIMED_WAITING(sleeping)(NativeMethod)$1.run(:282)(:619)复制代码
2.5.5TIMED_WAITING(parking)

当前线程被挂起一段时间,即正在执行(inttime)方法.

下面的线程正处于TIMED_WAITING状态,表示当前被挂起一段时间,时长为参数中指定的时长,如(blocker,l10000)。因此该线程当前不消耗CPU。

"RMITCP"daemonprio=6tid=0x0ae3b800nid=0x958waitingoncondition[0x17eff000..0x17effa94]:TIMED_WAITING(parking)(NativeMethod)-parkingtowaitfor0x02f49f58($TransferStack)(:179)$(:424)$(:323)(:871)(:495)$(:693)(:620)复制代码
2.5.6WAINTING(onobjectmonitor)

当前线程被挂起,即正在执行()方法(无参数的wait()方法).

下面的线程正处于WAITING状态,表示当前线程被挂起,如()(只能通过notify()唤醒)。因此该线程当前不消耗CPU。

"IPCClient"daemonprio=10tid=0x00002aaad4129800nid=0()[0x039000..0x039d00]:WAITING(onobjectmonitor)(NativeMethod)-waitingon0x00002aaab3acad18;($Connection)(:485)$(:234)-locked0x00002aaab3acad18($Connection)$(:273)复制代码
2.5.7总结

处于TIMED_WAITING、WAITING状态的线程一定不消耗CPU,处于Runable的线程,要结合当前线程代码的性质判断,是否消耗CPU。

如果纯java运算代码,则消耗CPU

如果是网络IO,很少消耗CPU

如果是本地代码,结合本地代码的性质判断(可以通过pstack/gstack获取本地线程堆栈),如果是纯运算代码,则消耗CPU,如果被挂起,则不消耗CPU,如果是IO,则不怎么消耗CPU。

03CPU飙高3.1概述

系统的CPU的飙高的原因有很多,下面罗列一下一些常见的场景:

3.2死循环导致CPU飙高

死循环并不一定会导致CPU的100%利用率,如果死循环中的代码不是CPU密集型,而是像Socket或者数据库IO操作,这些操作是不怎么消耗CPU的。但如果循环代码中是CPU密集型操作,那这就是导致CPU利用率100%的可能原因。

定位问题的方法是:通过多次打印线程堆栈

主要流程如下:

获取第一次堆栈信息

等待一定的时间,再获取第二次堆栈信息。

预处理两次堆栈信息,首先去掉处于sleeping或者waiting状态的线程,因为这种线程是不消耗CPU的

比较第一次堆栈和第二次堆栈预处理后的线程,找出这段时间一直活跃的线程,如果两次堆栈中同一个线程处于同样的调用上下文,那么就应该列为重点怀疑对象。具体情况需要结合代码详细分析。导致这种情况的可能原因猜测是:

猜测一:检查该线程的执行上下所对应的代码是否属于长期运行的代码。

猜测二:如果不属于长期运行的代码,那么这个线程为什么长期执行不完那段代码,可能的原因是代码出现死循环了。

导致死循环出现的原因:

多线程:for,while循环中的退出条件永远不满足导致的死循环。

多线程:链表等数据结构首尾相接,导致遍历永远无法停止

其问题

3.3非死循环导致CPU飙高

导致CPU飙高的可能原因:

纯java代码导致的CPU过高,具体情况可以参考博客:【稳定性平台】一次性能优化经验分享

java代码中调用的JNI代码导致的CPU过高

虚拟机自身的代码导致的CPU过高,比如GC的bug等

定位问题的方法是:通过打印线程堆栈

主要流程如下:

第一步:获取目标进程id(pid),使用命令可以获取进程ID,jps-l或ps-ef|grepjava

第二步:通过top-Hppid获取该进程下最消耗CPU的本地线程ID。

第三步:打印线程堆栈,使用如下命令,jstack-lpid

第四步:将十进制的本地线程ID换算成16进制,采用如下命令:printf"0x%x\n"53841(本地线程ID),输出Oxd251。

第五步:在java线程堆栈中查找nid=第一步获取的最耗CPU时间的线程ID(十进制换算成的十六进制的本地线程ID),具体分析如下

如果在java线程堆栈中找到了对应的线程ID,并且该线程正在执行纯java代码,说明是该java代码导致的CPU过高的。

如果在java线程堆栈中找到了对应的线程ID,并且该线程正在执行nativecode代码,说明是导致的CPU过高的问题代码在JNI调用中。

如果在java线程堆栈中找不到了对应的线程ID,有如下两种可能:

JNI调用中重新创建的线程来执行,那么在java线程堆栈中就不存在该线程的信息。虚拟机自身代码导致的CPU过高,如堆内存枯竭导致的频繁FullGC,或者虚拟机bug等

此时同样可以通过pstackpid命令获取所有的本地线程堆栈,根据之前获取的最耗时CPU时间的线程ID,在本地线程堆栈中找到对应的线程,即为高消耗CPU的线程。借助本地线程堆栈信息,可以直接定位到本地代码找出问题。

04系统假死4.1概述

系统假死只是一个表面现象。系统假死,从表面上看,是系统不处理响应。对于web系统来说,http请求无数据返回。总之来说,系统像死了一样。导致系统假死的原因很多,具体的问题需要在特定的场景下进行分析,但总的归结原因有如下几种:

备注:特殊情况,系统假死,当一个服务即对外提供http和rpc服务时,两者使用的不同的线程池,即使其中一个线程池中的线程产生死锁,另外一个线程池仍然可以提供服务

4.2线程死锁4.2.1死锁和定位方法

什么死锁

当两个线程或多个线程正在等待被对方占有的锁,死锁就会发生。死锁会导致两个线程无法继续运行,被永远挂起。

定位问题的方法是:打印线程堆栈。如出现死锁时,在打印线程堆栈时虚拟机会自动给出死锁的提示。

4.2.2背景

在2021.12.1311:15refund-interfaces服务上这台机器10.240.49.153操作命令挂载沙箱,在11:32:19分在流量回放平台操作录制,此时返回操作失败,对录制失败进行问题排查,在11:34分,order服务有接口报服务超时,但是从refund-interface监控上看,这台10.240.49.153的监控不存在,其他都是正常的,11:59才发现这台机器仍然在dubbo的注册中心,当时情况没有比较好手段保存现场,只能将机器重启,破会了现场。事后初步猜测是在高并发的情况下,操作平台的录制功能导致发生死锁。而发生死锁时,要想恢复系统,临时也是唯一规避的办法就是将系统重启。

为保证服务的可用性,在发生异常情况下,降级方案和问题排查思路

通常的降级方案是:

【方案一】保存现场法-摘除流量法,目前我司该方案并不完善

1.对于http,需要从网关摘除流量,即是consul

2.对于rpc,需要从dubbo的注册中心摘除流量,即是nacos

3.对于mq,服务接入MQ的消费端摘除流量

【方案二】万能法-机器重启

通常的问题排查思路是:尽量保存现场,便于事后问题排查!!!!

备注:对于死锁的发生,如没有保留现场,需要对其发现的手段是通过压测的方式。因为代码如有发生死锁的潜在可能并不意味照死锁每次都发生,它只发生该发生的时候,当死锁出现的时候,往往是遇到了最不幸的时候-在高负载的生产环境之下

4.2.3通过压测方式来复现死锁4.2.3.1现象

在2021-12-2711:58:15没有监控,压测是存在,服务进程是存在的,在12:02dump线程堆栈进行观察,如上

4.2.3.2dump线程堆栈
FoundoneJava-leveldeadlock:=============================":20888-thread-200":waitingtolockmonitor0x00007faa6c00c128(object0x00000006bd2a0000,),whichisheldby"""":waitingtolockmonitor0x00007fa91c0568b8(object0x00000006bcf0de00,),whichisheldby"XNIO-1task-44""XNIO-1task-44":waitingtolockmonitor0x00007fa904004848(object0x0000000722c10c48,),whichisheldby""Javastackinformationforthethreadslistedabove:===================================================":20888-thread-200":(:39)(:194)(:89)(:117)(:353)(:164)()$1.invoke(:81)(:147)$1.invoke(:81)(:38)$1.invoke(:81)(:41)$1.invoke(:81)$1.reply(:145)(:100)(:175)(:51)(:57)(:51)(:55)(:1149)$(:624)(:775)"":(:881)-waitingtolock0x00000006bcf0de00()(:662)(:755)(:142)(:468)$100(:74)$1.run(:369)$1.run(:363)(NativeMethod)(:362)$400(:22)$1.loadClass(:91)(:55)-locked0x0000000722c10c48()(:64)(:406)-locked0x00000006bd2a0000()(:47)-locked0x00000006bd2a0000()(:352)(:34)(:89)(:117)(:353)(:164)(:62)(:52)(:55)(:552)(:631)(:1845)-locked0x0000000722cc9928()(:1498)-locked0x0000000722cc9928()(:480)(:75)(:43)(:483)(:427)(:599)(:524)(:491)(:486)(:400)(:339)$(:420)(:51)(:55)$(:511)(:266)$$201(:180)$(:293)(:1149)$(:624)(:775)"XNIO-1task-44":(:53)-waitingtolock0x0000000722c10c48()(:64)(:352)(:51)(:76)(:117)(:353)(:164)()(:97)$3.executeSQL(:152)$3.executeSQL(:149)(:17)(:155)(:89)(:64)(:79)(:63)(:324)(:156)(:109)(:108)(:61)$(UnknownSource)(:147)(:140)(UnknownSource)(:43)(:498)$(:426)$(UnknownSource)(:223)(:147)(:80)(:93)$(UnknownSource)(:1049)$FastClassBySpringCGLIB$41(generated)(:218)$(:750)(:163)(:139)(:186)(:56)(:186)(:93)(:186)$(:689)$EnhancerBySpringCGLIB$28(generated)(:147)(:150)$FastClassBySpringCGLIB$(generated)(:218)$(:750)(:163)(:88)(:109)(UnknownSource)(:43)(:498)(:644)(:633)(:70)(:175)(:93)(:186)$(:689)$EnhancerBySpringCGLIB$25(generated)(UnknownSource)(:43)(:498)(:190)(:138)(:105)(:893)(:798)(:87)(:1040)(:943)(:1006)(:909)(:665)(:883)(:750)(:74)$(:129)(:28)(:61)$(:131)(:124)(:61)$(:131)(:88)(:119)(:61)$(:131)(:90)(:61)$(:131)(:100)(:119)(:61)$(:131)(:93)(:119)(:61)$(:131)(:94)(:119)(:61)$(:131)(:63)(:119)(:61)$(:131)(:114)(:104)(:119)(:61)$(:131)(:201)(:119)(:61)$(:131)(:170)(:61)$(:131)(:84)(:62)$1.handleRequest(:68)(:36)(:68)(:132)(:57)(:43)(:46)(:64)(:60)(:77)(:43)(:43)(:43)(:269)$100(:78)$2.call(:133)$2.call(:130)$1.call(:48)$1.call(:43)(:249)$000(:78)$1.handleRequest(:99)(:376)":(:53)-waitingtolock0x0000000722c10c48()(:64)(:352)(:51)$1.run(:830)(:51)(:55)(:1149)$(:624)(:775)Found1deadlock.复制代码

备注:

有一些令人头疼的死锁场景,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中,jstack等工具也不见得能够显示全部锁信息。

4.2.3.3dump文件分析

线程XNIO-1

XNIO-1task-44":(:53)-waitingtolock0x0000000722c10c48()(:64)(:352)(:51)复制代码

线程

"":(:881)-waitingtolock0x00000006bcf0de00()(:662)(:755)(:142)(:468)$100(:74)$1.run(:369)$1.run(:363)(NativeMethod)(:362)$400(:22)$1.loadClass(:91)(:55)-locked0x0000000722c10c48()(:64)(:406)-locked0x00000006bd2a0000()(:47)-locked0x00000006bd2a0000()(:352)(:34)复制代码

结合代码分析

线程XNIO-1

线程

4.2.3.4结论

在高并发情况,类加载加锁导致死锁的

4.2.3.5解决方案

提前加载RepeatCache和Tracer类

4.3线程假死

线程假死是线程一直处于运行中,不退出。

定位问题的方法是:打印线程堆栈

业务中需要使用spark进行数据处理,因此将spark任务上传到服务器执行,但是main线程一直hang住。通过打印线程堆栈,摘出其中的部分线程堆栈,如下:

"main"#1prio=5os_prio=0tid=0x00007f0d98009800nid=0x6240brunnable[0x00007f0d9ec49000]:(NativeMethod)(:116)(:171)(:141)(:107)(:150)(:180)-locked0x000000072028a7b8()(:133)(:64)(:81)(:63)(:45)(:514)(:475)(:362)(:1367)(:133)(:842)(:823)-locked0x0000000722c7fc40()(:448)(:241)(:198)(:49)$.create(:68)-locked0x0000000722c7fe18($)$.$anonfun$createConnectionFactory$1(:62)$$Lambda$1606/936828380.apply(UnknownSource)(:48)(:46)$lzycompute(:70)-locked0x0000000722c96c18()(:68)(:90)$anonfun$execute$1(:180)$Lambda$1492/203829039.apply(UnknownSource)$anonfun$executeQuery$1(:218)$Lambda$1516/376234567.apply(UnknownSource)$.withScope(:151)(:215)(:176)$lzycompute(:132)-locked0x0000000722ca3c78()(:131)$anonfun$runCommand$1(:989)$Lambda$1379/456240898.apply(UnknownSource)$.$anonfun$withNewExecutionId$5(:103)$$Lambda$1387/862090614.apply(UnknownSource)$.withSQLConfPropagated(:163)$.$anonfun$withNewExecutionId$1(:90)$$Lambda$1380/1008095885.apply(UnknownSource)(:775)$.withNewExecutionId(:64)(:989)(:438)(:415)(:301)(:817)(:111)Lockedownablesynchronizers:-None复制代码

通过对上面的堆栈分析可以发现main线程的状态为:RUNNABLE,且阻塞在方法

(:107),通过查看配置发现没有配置相关connectTimeout和socketTimeout等参数,通过配置上面超时参数之后,就出现其它的报错。

05系统运行越来越慢5.1概述

系统缓慢一般是由于如下几个原因造成的:

堆内存泄漏造成的内存不足,导致系统越来越慢,直到停止。

Xmx设置太小造成的堆内存不足,导致系统越来越慢,直到停止。

系统出现死循环,消耗了过多的CPU。参考上一节的内容

资源不足导致性系统运行越来越慢。

\

5.2内存泄漏5.2.1定位方式

判断系统是否存在内存泄漏的依据是:如果系统存在内存泄漏,那么在完成垃圾回收之后的内存值应该持续上升。如果在现场能观察到这个现象,说明系统存在内存泄漏。当怀疑一个系统存在内存泄漏的时候,首先使用FULLGC信息对内存泄漏进行一个初步确认,确认系统是否存在内存泄漏。只检查完全垃圾回收后的可用内存值是否一直再增大,步骤如下:

首先截取系统稳定运行以后的GC信息(如初始化已经完成),这个非常重要,非稳定运行期的信息无分析价值,因为你无法确认内存的增长是正常的增长还是由于内存泄漏导致的非正常增长。

过滤出FULLGC的行。只有FULLGC的行才有分析价值。因为完成GC后的内存是当前Java对象真正使用的内存数量。一般系统会有两种可能:

如果完成垃圾回收后的内存持续增长,大有一直增长到Xmx设定值的趋势,那么这个时候基本上就可以断定系统存在内存泄漏。如果当前完成垃圾回收后内存增长到一个值之后,又能回落,总体上处于一个动态平衡,那么内存泄漏基本可以排除。

通过如上内存使用趋势分析之后,基本上就能确定系统是否存在堆内存泄漏。当然这种GC信息分析只能告诉你系统是否存在堆内存泄漏,但具体哪里泄漏,它是无法告诉你的。内存泄漏的的精确定位,是要找到内存泄漏的具体位置,需要通过dump内存堆栈,通过其内存堆栈分析工具才能找到真正导致内存泄漏的类或者对象。

5.2.2案例

jvm监控:

内存堆栈分析:

通过内存堆栈,定位到如下代码

原因猜测:

1.通过jvm的内存监控上看,这个泄漏几天,而且泄漏的速度是一条直线(近似匀速),如果是业务调用导致内存泄漏应该会有起伏,猜测可能是定时任务调用dubbo的逻辑。

验证

5.3资源不足5.3.1特点和定位方法

特点:如资源不足,那么会存在大量的线程在等待资源,打印的线程堆栈如果具有这个特征,那么就说明该系统资源是瓶颈。对于资源不足导致性能下降,打印出的线程堆栈中会存在大量的线程停在同样的调用上下文中。

资源不足或者资源使用不恰当,表现出来往往是一个性能问题。系统越来越慢,并最终停止响应。遇到系统变慢等问题,定位问题的方法是打印线程堆栈。

5.3.2可能的原因

5.3.3案例

备注:当时没有对服务打印线程堆栈。

从监控上看,数据库的连接池已经打满(连接池当时配置的60)。从客户端角度来看,当时客户端单机的cpu、memory都没有使用满,从服务端(mysql)来看,db没有任何异常。因此可以看出瓶颈卡在数据库连接上。

从具体的业务场景来分析,该问题有两种解决方案:

方案一:资源隔离法。因为从该业务上看,只有流量上报和回放上报的接口的qps高,而平台的其他接口qps低,并且都是平台后台的基本操作的方法。将流量上报和回放上报使用一个数据库连接池,平台的其他接口使用另外一个连接池。

方案二:提高数据库的连接池,该方式有点缺点是会影响后台的页面操作,出现页面卡顿的情况。

06性能分析6.1概述

线程堆栈进行性能分析使用场景:多线程场合下的性能瓶颈定位。特别是锁的使用不当,导致的性能瓶颈。

改善性能意味着用更少的资源做更多的事。当线程的运行因某个特定资源受阻时,称之为受限该资源:受限数据库、受限对端的处理能力。

性能调优的终极目标是系统的CPU利用率接近100%。如果系统的CPU没有被充分利用,那么可能存在如下问题:施加的压力不足,可能被测试的系统没有被加入到足够的压力(负载),这时候可以通过增加压力,检测系统的响应时间、服务失败率和CPU使用率情况。如果增加压力,系统开始出现部分服务失败,系统的响应时间变慢,或者CPU的使用率无法再上升,那么此时的压力应该是系统的饱和压力。即此时的能力是系统当前的最大能力。

系统存在瓶颈:当系统在饱和压力下,如果CPU的使用率没有接近100%,那么说明这个系统性能的还有提升的空间。如果系统存在如下问题,那么可以使用线程堆栈查找性能瓶颈的方法进行问题定位。

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

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

系统性能随负载的增加逐渐下降。随着用户数目的增多,应用程序的运行越发缓慢。若干用户退出系统,应用程序便能够恢复正常运行状态。

6.2常见的性能瓶颈

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

不相关的两个函数,共用了一个锁或者不同的共享变量共用了一个锁,无谓地制造出了资源争用锁的粒度过大,对共享资源访问完成之后,没有将后续的代码放在synchronized同步代码块之外。

sleep的滥用:sleep只适合用在等待固定时长的场合,如果轮询代码中夹杂着sleep()调用,这种设计必然是一种糟糕的设计。这种设计在某些场合下会导致严重的性能瓶颈。

String+滥用

不恰当的线程模型:在多线程场合下,如果线程模型不恰当,也会使性能低下。

效率低下的SQL语句或者不恰当的数据库设计

线程数量不足:在使用线程池的场合,如果线程池的线程配置太少,也会导致性能低下。

内存泄漏导致的频繁GC:内存泄漏会导致GC越来越频繁,而GC操作是CPU密集型操作,频繁GC会导致系统整体性能严重下降。

6.3压测发现性能瓶

性能瓶颈的特征

当前的性能瓶颈只有一处,只有当解决的这一块,才知道下一处。没有解决当前的性能瓶颈,下一处性能瓶颈是不会出现的

性能瓶颈是动态的,低负载下不是瓶颈的地方,在高负载下可能成为瓶颈。

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

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

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

线程的数量过少

锁的粒度过大导致的锁竞争。

资源竞争(如数据库连接池中连接不足,导致有些企图获取连接的线程被阻塞)

锁范围内有大量耗时操作(如大量的磁盘IO),导致锁争用。

远程通信的对方处理缓慢(甚至导致socket缓冲区写满),如数据库侧的SQL代码性能低下。

绝大多数线程处于等待状态,只有几个工作的线程,总体性能上不去。可能的原因是,系统存在关键路径,在该关键路径上没有足够的能力给下个阶段输送大量的任务,导致其它地方空闲。如在消息分发系统,消息分发一般是一个线程,而消息处理是多个线程,这时候消息分发是瓶颈的话,那么从线程堆栈就会观察到上面提到的现象:即该关键路径没有足够的能力给下个阶段输送大量的任务,导致其它地方空闲。

线程总的数量很少。导致性能瓶颈的原因与上面的类似。这里线程很少,是由于某些线程池实现使用另一种设计思路,当任务来了之后才new出线程来,这种实现方式下,线程的数量上不去,就意味有在某处关键路径上没有足够的能力给下个阶段输送大量的任务,从而不需要更多的线程来处理。

6.5线程堆栈进行性能分析总结

07总结

本文是在阅读《java问题定位技术》等相关书籍和文档,结合发生在生产环境下的真实案例下进行梳理和总结,一方面提升问题排查能力,同时也对其进行总结和提炼,另外一方面希望通过总结和提炼本文能够帮助到大家提升问题排查能力和对线程堆栈的充分认识。

猜你喜欢
    不容错过