采用Java开发的大型应用系统越来越大,越来越复杂,很多系统集成在一起,整个系统看起来像个黑盒子。系统运行遭遇问题(系统停止响应,运行越来越慢,或者性能低下,甚至系统宕掉),如何速度命中问题的根本原因是我们接下来讲的目的。本系列文章将Java问题定位的方法体系化,提供一种以黑盒子方式进行问题定位的思路:如何使用线程堆栈进行性能瓶颈分析?如何分析内存泄漏?如何分析系统挂死?
目录
- 总述
- 如何输出线程堆栈?
- 如何解读线程堆栈?
- 线程的解读
- 锁的解读
- 线程状态的解读
总述
- 系统无缘无故的cpu过高
- 系统挂起,无响应
- 系统运行越来越慢
- 性能瓶颈(如无法充分利用cpu等)
- 线程死锁,死循环等
- 由于线程数量太多导致的内存溢出(如无法创建线程等)
借助线程堆栈可以帮助我们缩小范围,找到突破口。线程堆栈分析很多时候不需要源代码,在很多场合都有优势。下面我们就开始我们的线程堆栈之旅。
如何输出线程堆栈?
如何解读线程堆栈?
线程的解读
通过上节的介绍的方法打印堆栈信息,我们只关注java用户线程,其他由虚拟机自动创建的,在实际分析中,只关心java用户线程即可。
- <span style="font-family: SimSun;"><span style="font-size: 12px;">"main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]
- at java.lang.String.indexOf(String.java:1352)
- at java.io.PrintStream.write(PrintStream.java:460)
- - locked <0xc8bf87d8> (a java.io.PrintStream)
- at java.io.PrintStream.print(PrintStream.java:602)
- at MyTest.fun2(MyTest.java:16)
- - locked <0xc8c1a098> (a java.lang.Object)
- at MyTest.fun1(MyTest.java:8)
- - locked <0xc8c1a090> (a java.lang.Object)
- at MyTest.main(MyTest.java:26)</span></span>
"main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]
at java.lang.String.indexOf(String.java:1352)
at java.io.PrintStream.write(PrintStream.java:460)
- locked <0xc8bf87d8> (a java.io.PrintStream)
at java.io.PrintStream.print(PrintStream.java:602)
at MyTest.fun2(MyTest.java:16)
- locked <0xc8c1a098> (a java.lang.Object)
at MyTest.fun1(MyTest.java:8)
- locked <0xc8c1a090> (a java.lang.Object)
at MyTest.main(MyTest.java:26)
从上面的main线程看,线程堆栈里面的最直观的信息是当前线程的调用上下文,即从哪个函数调用到哪个函数(从下往上看),正执行到哪一类的哪一行,借助这些信息,我们就对当前系统正在做什么一目了然。
另外,从main线程的堆栈中,有-locked<0xc8c1a090>(a java.lang.Object) 语句,这表示该线程已经占用了锁,其中0xc8c1a090表示锁ID,这个锁ID是系统自动生成的,我们只需要知道每次打印堆栈,同一个ID表示是同一个锁即可
其中"线程对应的本地线程Id号"所指的本地线程是指该java虚拟机所对应的虚拟机中的本地线程,我们知道java是解析型语言,执行的实体是java虚拟机,因此java代码是依附于java虚拟机的本地线程执行的,之前文章中讲过,当启动一个线程时,是创建一个native本地线程,本地线程才是真实的线程实体,为了更加深入理解本地线程和java线程的关系,我们可以通过以下方式将java虚拟机的本地线程打印出来:
1、试用ps -ef|grep java 获得java进行id
2、试用pstack<java pid> 获得java虚拟机本地线程的堆栈
从操作系统打印出来的虚拟机的本地线程看,本地线程数量和java线程数量是相同的,说明二者是一一对应的关系。
我们获取的本地线程堆栈如下:
这个本地线程号如何与java线程堆栈文件对应起来呢,每一个线程都有tid,nid的属性,通过这些属性可以对应相应的本地线程,我们先看java线程第一行,里面有一个属性是nid,
main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]
其中nid是native thread id,也就是本地线程中的LWPID,二者是相同的,只不过java线程中的nid用16进制表示,本地线程的id用十进制表示。3368的十六进制表示0xd28,在java线程堆栈中查找nid为0xd28就是本地线程对应的java线程。
锁的解读
- wait() 当线程执行到wait()方法上,当前线程会释放监视锁,此时其他线程可以占有该锁,一旦wait()方法执行完成,当前线程继续持有该锁,直到执行完锁的作用域。结合notify(),可以实现两个线程之间的通信,一个线程可以通过这种方法通知另一个线程继续执行,完成线程之间的配合。wait()锁的示意图
在wait(5000)这个期间,当前线程会释放它占用的锁,其他线程有机会获得到该锁,当wait(5000)结束后,当前线程继续获取该锁的使用权。满足以下条件之一,wait退出:
1、达到等待时间之后,自动退出
2、其他线程调用了该锁的notify方法,如果多个线程在等待同一个锁,只有一个线程会被通知到。
- sleep() 和锁操作无关,如果该方法恰好在一个锁的保护范围内,当前线程即使执行sleep的时候,仍然保持监视锁。
sleep方法是线程的一个静态方法,实际上和锁操作无关,不会产生特别的锁,如果原来持有,现在仍然持有,如果原来没有,现在仍然没有。
从上面介绍的线程堆栈看,线程堆栈中包含直接信息为:线程个数,每个线程调用的方法堆栈,当前锁的状态。从线程个数可以直接数出来,线程调用的方法堆栈,从下向上看,表示了当前线程调用哪个类哪个方法,锁的状态看起来需要一些技巧,与锁相关的重要信息如下:
- 当一个线程占有一个锁的时候,线程堆栈会打印一个-locked<0x22bffb60>
- 当一个线程正在等在其他线程释放该锁,线程堆栈会打印一个-waiting to lock<0x22bffb60>
- 当一个线程占有一个锁,但又执行在该锁的wait上,线程堆栈中首先打印locked,然后打印-waiting on <0x22c03c60>
线程状态的解读
- RUNNABLE 从虚拟机的角度看,线程正在运行状态。
- TIMED_WAITING(on object monitor)表示当前线程被挂起一段时间,说明该线程正在执行obj.wait(ing time)方法,该线程不消耗cpu。
- TIMED_WAITING(sleeping) 表示当前线程被挂起一段时间,正在执行Thread.sleep(int time )方法,如:
- WAITING(on object monitor)当前线程被挂起,正在执行无参数的obj.wait()方法,只能通过notify唤醒,因此不消耗cpu
- 处于timed_waiting,waiting状态的线程一定不消耗cpu,处于runnable状态的线程不一定会消耗cpu,要结合当前线程代码的性质判断,是否消耗cpu
- 如果是纯java运算代码,则消耗cpu
- 如果网络io,很少消耗cpu
-
如果是本地代码,集合本地代码的性质,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu。