一、常用命令:
在JDK的bin目彔下,包含了java命令及其他实用工具。
jps:查看本机的Java中进程信息。
jstack:打印线程的栈信息,制作线程Dump。
jmap:打印内存映射,制作堆Dump。
jstat:性能监控工具。
jhat:内存分析工具。
jconsole:简易的可视化控制台。
jvisualvm:功能强大的控制台。
二、认识Java Dump:
什么是Java Dump?
Java虚拟机的运行时快照。将Java虚拟机运行时的状态和信息保存到文件。
堆Dump,包含线程Dump,幵包含所有堆对象的状态。二进制格式。
线程Dump,包含所有线程的运行状态。纯文本格式。线程栈是瞬时记录,一般都需要结合程序的日志进行跟踪问题。
线程堆栈包括每一个线程的调用堆栈,锁的持有情况。线程堆栈的信息都包含:
1、线程名字,id,线程的数量等。
2、线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等)
3、调用堆栈包含完整的类名,所执行的方法,源代码的行数等
不同虚拟机打印堆栈略有些不同。
Java Dump有什么用?
补足传统Bug分析手段的不足: 可在任何Java环境使用;信息量充足。 针对非功能正确性的Bug,主要为:多线程幵发、内存泄漏。
1. 内存泄露,常见的是程序里load大量的数据到缓存;
2. 线程死锁,死循环;
3.cpu过高,分析原因;
4.系统运行越来越慢,交易响应时间长,甚至系统被挂起没有响应;
5.由于线程数量太多导致的内存溢出(如无法创建线程等);
6.cpu过低,没有充分利用cpu资源;
制作Java Dump
使用Java虚拟机制作Dump
指示虚拟机在发生内存不足错误时,自动生成堆Dump
-XX:+HeapDumpOnOutOfMemoryError
使用图形化工具制作Dump
使用JDK(1.6)自带的工具:Java VisualVM,两种都可以生成。
jmap
:打印内存映射,制作堆Dump。
步骤:
- 检查虚拟机版本(java -version)
- 找出目标Java应用的进程ID(jps)
- 使用jstack命令制作线程Dump • Linux环境下使用kill命令制作线程Dump
- 使用jmap命令制作堆Dump
如何抓取Thread Dump
一般当服务器挂起,崩溃或者性能底下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析. 在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做threaddump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。
有很多方式可用于获取ThreadDump, 下面列出一部分获取方式:
操作系统命令获取ThreadDump:
Windows:
1.转向服务器的标准输出窗口并按下Control + Break组合键, 之后需要将线程堆栈复制到文件中;
UNIX/ Linux:
首先查找到服务器的进程号(process id), 然后获取线程堆栈.
1. ps –ef | grep java
2. kill -3 <pid>
注意:一定要谨慎, 一步不慎就可能让服务器进程被杀死。kill -9 命令会杀死进程。
JVM 自带的工具获取线程堆栈:
JDK自带命令行工具获取PID,再获取ThreadDump:
1. jps 或 ps –ef|grepjava (获取PID)
2. jstack [-l ]<pid> | tee -a jstack.log (获取ThreadDump)
java线程的状态转换
2.1 新建状态(New)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。
2.2 就绪状态(Runnable)
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。
2.3 运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。
2.4 阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为以下3种:
1)位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
2)位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
3)其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
2.5 死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
值得关注的线程状态有:
- 死锁,Deadlock(重点关注)
- 执行中,Runnable
- 等待资源,Waiting on condition(重点关注)
- 等待获取监视器,Waiting on monitor entry(重点关注)
- 暂停,Suspended
- 对象等待中,Object.wait() 或 TIMED_WAITING
- 阻塞,Blocked(重点关注)
- 停止,Parked
含义如下所示:
- Deadlock:死锁线程,一般指多个线程调用间,进入相互资源占用,导致一直等待无法释放的情况。
- Runnable:一般指该线程正在执行状态中,该线程占用了资源,正在处理某个请求,有可能正在传递SQL到数据库执行,有可能在对某个文件操作,有可能进行数据类型等转换。
- Waiting on condition:等待资源,或等待某个条件的发生。具体原因需结合 stacktrace来分析。
- 如果堆栈信息明确是应用代码,则证明该线程正在等待资源。一般是大量读取某资源,且该资源采用了资源锁的情况下,线程进入等待状态,等待资源的读取。
- 又或者,正在等待其他线程的执行等。
- 如果发现有大量的线程都在处在 Wait on condition,从线程 stack看,正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。
- 一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;
- 另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。
- 另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。
- Blocked:线程阻塞,是指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。
- Waiting for monitor entry 和 in Object.wait():Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。从下图1中可以看出,每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。
dump分析工具
- IBM Thread and Monitor Dump Analyze for Java jcaNNN.jar 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
- http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。
- jvisualVM,默认打开 *.tdump格式的线程dump文件。Thread Dump Analyzer (TDA) 该工具可做为visualVM的插件。
Thread Dump分析
通过前面1.4部分的方法,获取Thread Dump信息后,对其进行分析;
3.1 首先介绍一下Thread Dump信息的各个部分
头部信息:
时间,jvm信息
2011-11-02 19:05:06
Full thread dump Java HotSpot(TM) Server VM (16.3-b01 mixed mode):
线程info信息块:
1. "Timer-0" daemon prio=10tid=0xac190c00 nid=0xaef in Object.wait() [0xae77d000]
2. java.lang.Thread.State: TIMED_WAITING (on object monitor)
3. atjava.lang.Object.wait(Native Method)
4. -waiting on <0xb3885f60> (a java.util.TaskQueue) ###继续wait
5. atjava.util.TimerThread.mainLoop(Timer.java:509)
6. -locked <0xb3885f60> (a java.util.TaskQueue) ###已经locked
7. at java.util.TimerThread.run(Timer.java:462)
* 线程名称:Timer-0
* 线程类型:daemon
* 优先级: 10,默认是5
* jvm线程id:tid=0xac190c00,jvm内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现。)
* 对应系统线程id(NativeThread ID):nid=0xaef,指该java虚拟机所对应的虚拟机中的本地线程,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
* 线程状态:in Object.wait().
* 起始栈地址:[0xae77d000]
* Java thread statck trace:是上面2-7行的信息。到目前为止这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。
at java.util.TimerThread.run(Timer.java:462)
462:正在调用函数的源代码行数
Timer.java:正在调用函数的源文件
run:正在调用函数的方法名
java.util.TimerThread:正在调用函数的类名与路径
这里只是举个例子,实际常看的还是程序代码部分。
对于thread dump信息,主要关注的是线程的状态和其执行堆栈。现在针对这两个重点部分进行讲解:
1)Java thread statck trace详解:
线程堆栈里面的最直观的信息是当前线程的调用上下文,堆栈信息应该逆向解读:程序先执行的是第7行,然后是第6行,依次类推,即从哪个函数调用到哪个函数(从下往上看),正执行到哪一类的哪一行。
- locked <0xb3885f60> (a java.util.ArrayList)
- waiting on <0xb3885f60> (a java.util.ArrayList)
也就是说对象先上锁,锁住对象0xb3885f60,然后释放该对象锁,进入waiting状态。
为啥会出现这样的情况呢?看看下面的java代码示例,就会明白:
synchronized(obj) {
.........
obj.wait();
.........
}
在堆栈的第一行信息中,进一步标明了线程在代码级的状态,例如:
java.lang.Thread.State: TIMED_WAITING (parking)
2) 线程状态详解:
Runnable
_The thread is either running or ready to run when it gets its CPU turn._
Wait on condition
_The thread is either sleeping or waiting to be notified by another thread._
该状态出现在线程等待某个条件的发生或者sleep。具体是什么原因,可以结合 stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。在Java引入 New IO之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 New IO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。
如果发现有大量的线程都处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,看是否很明显超过了所在网络带宽的限制;观察cpu的利用率,看系统态的CPU时间是否明显大于用户态的CPU时间;如果程序运行在 Solaris 10平台上,可以用dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了,将被唤醒。
Waiting for Monitor Entry and in Object.wait()
_The thread is waiting to getthe lock for an object (some other thread may be holding the lock). Thishappens if two or more threads try to execute synchronized code. Note that thelock is always for an object and not for individual methods._
在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “ActiveThread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitorentry”,而在 “Wait Set”中等待的线程状态是“in Object.wait()”。
先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。对应的 code就像:
synchronized(obj) {
.........
}
这时有两种可能性:
该 monitor不被其它线程拥有, Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码。
该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。
在第一种情况下,线程将处于 “Runnable”的状态,而第二种情况下,线程 DUMP会显示处于 “waiting for monitor entry”。
临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这和我们多线程的程序的初衷是相反的。如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。
再看“Wait Set”里面的线程。当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll(),“Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的Monitor,恢复到运行态。在 “Wait Set”中的线程, DUMP中表现为: in Object.wait()。
一般,Cpu很忙时,则关注runnable的线程,Cpu很闲时,则关注waiting for monitor entry的线程。
3.2 JVM线程介绍
在Thread Dump中,有一些 JVM内部的后台线程,来执行譬如垃圾回收,或者低内存的检测等等任务,这些线程往往在 JVM初始化的时候就存在,如下所示:
HotSpot VM Thread
被HotSpot VM管理的内部线程为了完成内部本地操作,一般来说不需要担心它们,除非CPU很高。
"VM Periodic Task Thread" prio=10tid=0xad909400 nid=0xaed waiting on condition
HotSpot GC Thread
当使用HotSpot parallel GC,HotSpot VM默认创建一定数目的GC thread。
"GC task thread#0 (ParallelGC)"prio=10 tid=0xf690b400 nid=0xade runnable
"GC task thread#1 (ParallelGC)"prio=10 tid=0xf690cc00 nid=0xadf runnable
"GC task thread#2 (ParallelGC)"prio=10 tid=0xf690e000 nid=0xae0 runnable
……
当面对过多GC,内存泄露等问题时,这些是关键的数据。使用native id,可以将从OS/Java进程观测到的高CPU与这些线程关联起来。
JNI global references count
JNI global reference是基本的对象引用,从本地代码到被Java GC管理的Java对象的引用。其角色是阻止仍然被本地代码使用的对象集合,但在Java代码中没有引用。在探测JNI相关内存泄露时,关注JNI references很重要。如果你的程序直接使用JNI或使用第三方工具,如检测工具,检测本地内存泄露。
JNI global references: 832
Java Heap utilization view
从jdk1.6开始在thread dump快照底部,可以找到崩溃点的内存空间利用情况:YongGen,OldGen和PermGen。目前我测试的系统导出的thread dump,还未见到这一部分内容(sun jdk1.6)。以下例子,摘自他人文章:
Heap
PSYoungGen total 466944K, used 178734K [0xffffffff45c00000, 0xffffffff70800000, 0xffffffff70800000)
eden space 233472K, 76% used [0xffffffff45c00000,0xffffffff50ab7c50,0xffffffff54000000)
from space 233472K, 0% used [0xffffffff62400000,0xffffffff62400000,0xffffffff70800000)
to space 233472K, 0% used [0xffffffff54000000,0xffffffff54000000,0xffffffff62400000)
PSOldGen total 1400832K, used 1400831K [0xfffffffef0400000, 0xffffffff45c00000, 0xffffffff45c00000)
object space 1400832K, 99% used [0xfffffffef0400000,0xffffffff45bfffb8,0xffffffff45c00000)
PSPermGen total 262144K, used 248475K [0xfffffffed0400000, 0xfffffffee0400000, 0xfffffffef0400000)
object space 262144K, 94% used [0xfffffffed0400000,0xfffffffedf6a6f08,0xfffffffee0400000)
还有一些其他的线程(如下),不一一介绍了,有兴趣,可查看文章最后的附件信息。
"Low Memory Detector" daemon prio=10tid=0xad907400 nid=0xaec runnable [0x00000000]
"CompilerThread1" daemon prio=10tid=0xad905400 nid=0xaeb waiting on condition [0x00000000]
"CompilerThread0" daemon prio=10tid=0xad903c00 nid=0xaea waiting on condition [0x00000000]
"Signal Dispatcher" daemon prio=10tid=0xad902400 nid=0xae9 runnable [0x00000000]
"Finalizer" daemon prio=10tid=0xf69eec00 nid=0xae8 in Object.wait() [0xaf17d000]
"Reference Handler" daemon prio=10tid=0xf69ed800 nid=0xae7 in Object.wait() [0xae1e7000]
"VM Thread" prio=10 tid=0xf69e9800nid=0xae6 runnable
四、案例分析:
4.1、使用方案
cpu飙高,load高,响应很慢
方案:
* 一个请求过程中多次dump
* 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了。
查找占用cpu最多的线程信息
方案:
* 使用命令: top -H -p pid(pid为被测系统的进程号),找到导致cpu高的线程id。
上述Top命令找到的线程id,对应着dump thread信息中线程的nid,只不过一个是十进制,一个是十六进制。
* 在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息。
cpu使用率不高但是响应很慢
方案:
* 进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因。
请求无法响应
方案:
* 多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,恭喜你,锁住了!
4.2 案例分析:
1.死锁:
死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。
在thread dump中,会看到类似于这样的信息:
(图 1)
(图2)
说明:
(图1)中有一个“Waiting for monitor entry”,可以看出,两个线程各持有一个锁,又在等待另一个锁,很明显这两个线程互相持有对方正在等待的锁。所以造成了死锁现象;
(图2)中对死锁的现象做了说明,可以看到,是“DeadLockTest.java”的39行造成的死锁现象。这样就能到相应的代码下去查看,定位问题。
与锁相关的重要信息有:
- 当一个线程占有一个锁的时候,locked<0x276ea028>
- 当一个线程正在等在其他线程释放该锁,waiting to lock<0x276ea028>
- 当一个线程占有一个锁,但又执行在该锁的wait上,线程堆栈中会先打印locked,再打印waiting on <0x276ea028>
通常情况下,多线程中如果一些线程正在等待锁时,应该有一个线程占用了这个锁。即locked与waiting to lock的关系
2.热锁
热锁,也往往是导致系统性能瓶颈的主要因素。其表现特征为:由于多个线程对临界区,或者锁的竞争,可能出现:
* 频繁的线程的上下文切换:从操作系统对线程的调度来看,当线程在等待资源而阻塞的时候,操作系统会将之切换出来,放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
* 大量的系统调用:因为线程的上下文切换,以及热锁的竞争,或者临界区的频繁的进出,都可能导致大量的系统调用。
* 大部分CPU开销用在“系统态 ”:线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,虽然系统很忙碌,但是 CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。
* 随着 CPU数目的增多,系统的性能反而下降。因为CPU数目多,同时运行的线程就越多,可能就会造成更频繁的线程上下文切换和系统态的CPU开销,从而导致更糟糕的性能。
上面的描述,都是一个 scalability(可扩展性)很差的系统的表现。从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。
那么,怎么去了解 “热锁 ”出现在什么地方呢?一个重要的方法还是结合操作系统的各种工具观察系统资源使用状况,以及收集Java线程的DUMP信息,看线程都阻塞在什么方法上,了解原因,才能找到对应的解决方法。
我们曾经遇到过这样的例子,程序运行时,出现了以上指出的各种现象,通过观察操作系统的资源使用统计信息,以及线程 DUMP信息,确定了程序中热锁的存在,并发现大多数的线程状态都是 Waiting for monitor entry或者 Wait on monitor,且是阻塞在压缩和解压缩的方法上。后来采用第三方的压缩包 javalib替代 JDK自带的压缩包后,系统的性能提高了几倍。
参考资料:
性能分析之-- JAVA Thread Dump 分析综述
Thread Dump线程死锁检测工具(JStack)的使用详解