如何分析Thread Dump(收集)--Thread Dump可以解决的问题

转载:https://www.cnblogs.com/kongzhongqijing/articles/4152072.html

 jstack  pid 可以打出来 thread dump info

  当Java Web程序运行变慢,或者发生故障时,需要使用Thread Dumps。分析Java中的线程,线程如何创建的,如何管理线程,怎么从运行中的程序中dump 线程,最后怎么分析他们得到阻塞和存在瓶颈的线程。 

   

Java和线程

    一个web server使用几十到几百条线程去处理大量的并发用户。如果多条线程使用共享的资源,无法避免线程之间对数据的竞争,有时候还会发生死锁。
    线程竞争是web程序上不同的线程去访问共享资源,一条线程等待另外线程释放锁。例如,在记录log的时候,线程记录log时,必须先获得锁,然后去再访问共享资源。死锁是一种特殊的线程竞争,两个或多个线程要完成自己的任务,都要必须要等待其他的线程完成他们的任务。线程竞争会带来各种不同的问题,为了分析这些问题,需要使用Thread Dump。Thread Dump记录了每个线程真正的状态。

   线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间状态的快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。

   线程堆栈的信息都包含

   1、线程名字,id,线程的数量等。

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

   3、调用堆栈包含完整的类名,所执行的方法,源代码的行数等

   不同虚拟机打印堆栈略有些不同。

   线程作用:

   线程栈是瞬时快照包含线程状态以及调用关系,可以借助堆栈信息帮助分析很多问题,比如线程死锁,锁争用,死循环,识别耗时操作等等。线程栈是瞬时记录,所以没有历史消息的回溯,经常需要打印几次做对比分析,同时一般都需要结合程序的日志进行跟踪。

   线程栈能分析如下性能问题:

1、系统cpu过高
2、性能瓶颈:如响应时间长但CPU资源并不高
3、系统运行越来越慢,响应时间长
4、系统挂起,长时间无响应或响应时间长
5、线程死锁,死循环等
6、由于线程数量太多导致的内存溢出(如无法创建线程等)

 
Java线程的背景
    线程同步
     多条线程之间可以同时执行,为了确保多线程在使用共享资源上面的通用性,使用线程同步保证在同一时间只能有一条线程可以访问共享资源。
     线程同步在Java中可以使用监视器。每个Java对象都有一个监视器,这个监视器只能被一个线程拥有。当一个线程要获得另外线程拥有的监视器时,需要进入等待队列直到线程释放监视器。
    线程的状态
    为了分析Thread Dump ,需要先了解线程的状态。线程的状态是在java.lang.Thread.State中。

 
  1. 新建状态(New) 新创建了一个线程对象。

  2. 就绪状态(Runnable) 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running) 就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked) 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Jstack使用的关键字描述的线程状态与上边线程不太一样,所以可能理解上可能会出现混淆。虽然Java中的线程一样有上述中描述的5种状态,但在实际情况下线程新建状态和死亡状态持续很短,我们也并不太关心。大多时候我们关注的是运行状态/阻塞状态,

 下面这个图很重要

 
详细过程可参考: JVM中线程的状态转换图
  • 当执行new Thread(Runnabler)后,新创建出来的线程处于new状态,这种线程不可能执行
  • 当执行thread.start()后,线程处于runnable状态,这种情况下只要得到CPU,就可以开始执行了。runnable状态的线程,会接受JVM的调度,进入running状态,但是具体何时会进入这个状态,是随机不可知的
  • running状态中的线程最为复杂,可能会进入runnable、waiting、timed_waiting、blocked、dead状态:
  • 如果CPU调度给了别的线程,或者执行了Thread.yield()方法,则进入runnable状态,但是也有可能立刻又进入running状态
  • 如果执行了Thread.sleep(long),或者thread.join(long),或者在锁对象上调用object.wait(long)方法,则会进入timed_waiting状态
  • 如果执行了thread.join(),或者在锁对象上调用了object.wait()方法,则会进入waiting状态
  • 如果进入了同步方法或者同步代码块,没有获取锁对象的话,则会进入blocked状态
  • 处于waiting状态中的线程,如果是因为thread.join()方法进入等待的,在目标thread执行完毕之后,会回到runnable状态;如果是因为object.wait()方法进入等待的话,在锁对象执行object.notify()或者object.notifyAll()之后会回到runnable状态
  • 处于timed_waiting状态中的线程,和waiting状态中的差不多,只不过是设定时间到了,就会回到runnable状态
  • 处于blocked状态中的线程,只有获取了锁之后,才会脱离阻塞状态
  • 当线程执行完毕,或者抛出了未捕获的异常之后,会进入dead状态,该线程结束
 

代码中共有除RUNNING之外的6种状态,为了表示线程正在执行,特加了RUNNING这种状态。

我们需要重点关注RUNNABLE、BLOCKED、WAITING和TIME_WAITING四种状态,jstack打印的线程堆栈中也会时时出现。代码示例

1)BLOCKED:就是线程在等待获取锁进入同步块或者同步方法中。两个死锁的线程即是Blocked。

2)WAITING:比BLOCKED状态进步一些,指已经获得锁了,但由于有些条件不满足,要自己等会,调用object.wait()方法。等条件满足了,别的线程调用notify再叫我。另外也可以调用Thread.join()方法,顾名思义就是调用别的线程的join方法,让别人join进来先执行,那我就只能等会了。但是由于wait()和notify()以及notifyAll()用于协调对共享资源的存取,所以必须在synchronized块中使用。所以即便wait状态的线程被notfiy唤醒了,也需要再次获得锁,所以唤醒后进入Blocked状态。

3)TIMED_WAITING:类比WAITING,差异是不需要notify()或者notifyAlL()方法唤醒,时间到了自己醒了。另外sleep比较好理解,就是让当前线程睡一会,与wait的区别是它不释放锁

4)RUNNABLE不用多说,在JAVA虚拟机中已经在运行,但是在等待操作系统资源,比如CPU时间片。

 
 
NEW:
线程被创建但是还没有被执行(已经new了还没有调用start()方法)。在线程dump文件里很少。
 
RUNNABLE:
线程正在占用cpu并且在执行任务

从虚拟机的角度看,线程正在运行状态,状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等。从OS系统角度分析,这个状态是指的就绪态,只有获取到CPU时间片后,才能变成running实际运行。

处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。

 

BLOCKED:
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,线程为了获得监视器需要等待其他线程释放锁
 
Blocked(on object monitor),线程处于阻塞状态,正在等待一个monitor lock。通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。
 
比如:一台车不同时间段多人可以共用,但同一时间只有一个人用。用户A开走后,用户B想用用不了就被BLOCKED了。车可以理解为锁,用户A可以理解为线程A,用户B可以理解为线程B。因为用户A已经获取锁(车),线程B被blocked在锁(车)上了,得等待线程A释放锁后,线程B才可能获取到锁(车)。
 
WAITING:
指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll一遍该线程可以继续下一步操作。
这里要区分 BLOCKED 和 WATING 的区别:一个是在临界点外面等待进入, 一个是在临界点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束, 处于waiting状态的线程基本不消耗CPU

会进入WAITING状态的方法:

1、Object#wait() 而且不加超时参数
2、Thread#join() 而且不加超时参数
3、LockSupport#park()
4、ReentrantLock.lock() 无参方法调用

 

wait() 使得线程进入阻塞状态,它有两种形式:

一种允许指定以毫秒为单位的一段时间作为参数;另一种没有参数。前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态即就绪状态,后者则必须对应的 notify()或notifyAll()方法才能被唤醒,线程被唤醒后会进入锁池,等待获取锁标记。当调用wait()后会进入等待队列,线程会释放掉它所占有的“锁标志”从而使线程所在对象中的其它synchronized数据可被别的线程使用

 

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的线程。

 

TIMED_WAITING:
调用了sleep,wait,join,park方法使线程等待--有限期等待
这个与 WAITING 不同是通过方法参数指定了最大等待时间,WAITING 可以通过时间或者是外部的变化解除
线程栈中线程状态为waiting on condition,java.lang.Thread.State中常为time_waiting(),常需要结合看等待的条件是什么。

可能会进入TIMED_WAITING的方法:

1、Thread#sleep()
2、Object#wait() 并加了超时参数
3、Thread#join() 并加了超时参数
3、LockSupport#parkNanos()
4、LockSupport#parkUntil() 
5、Lock.tryLock(timeout, timeUnit)

 

TERMINATED

线程终止,很少看到该状态的线程栈 

 

状态小结: 

这些状态中NEW状态是开始,TERMINATED是销毁,在整个线程对象的运行过程中,这个两个状态只能出现一次。其他任何状态都可以出现多次,彼此之间可以相互转换
1)处于timed_waiting/waiting状态的线程一定不消耗cpu,处于runnable状态的线程不一定会消耗cpu,要结合当前线程代码的性质判断,是否消耗cpu
2)如果是纯java运算代码,则消耗cpu
3)如果线程处于网络io,很少消耗cpu
4)如果是本地代码,通过查看代码,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu。

 

 
线程类型
java中线程可以分为两种:
    1.    后台线程
    2.    非后台线程
当没有其他的非后台线程运行时后台线程将会终止。即使你不创建线程,java应用默认也会创建很多线程。这些大多数都是后台线程,主要为了执行gc或者jmx等类型的任务。 
从 'static void main(String[] args)’方法中开启的线程叫做非后台线程,当这些线程停止时,其他的所有后台线程也会停止()
 

在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 

 
 

获得一个Thread Dump

 
将会介绍三种常用的方法。请注意还会有其他很多方法可以获取Thread Dump。一个Thread dump仅仅可以显示测量时的线程状态。所以为了查看线程状态的变化,建议5到10次,每次间隔5秒。
 
kill -3 生成的dump文件默认不是独立的文件,而jstack命令则是生成的独立文件。
Solaris OS 下,使用kill -QUIT。---这个待实验。
 
linux/unix
 
linux下执行Kill -3 PID可以生成jvm的thread dump 
1)  $ ps - ef | grep java   获得当前正在运行的java进程pid
2) kill -3 <pid>  
注意生成的dump信息位置,如果nohup启动则会在nohup日志中,如果是按sh start.sh这种形式,则默认在日志文件中(tomcat的为catalina.out)
必要性:比如openJDK无法使用jstack命令时,就可以使用kill -3的方式来dump。
 
 
使用Java命令jstack获得Thread dump

1)获取java进程pid,获取方式如jps -v命令,ps -ef|grep java命令,top命令等等。。

2)jstack -f <pid>   flyer:我自己试了 jstack  pid 可以打出来

jstack命令有几项参数,根据实际需要获取。
dump的信息为独立的文件。
 
使用jvisualvm工具获得Thread dump
 
Jvisualvm工具连接指定Java,使用工具生成。
分析
左边的标记,当前正在运行的进程。点击想查看的进程,选择现场选项来查看实时的线程信息。点击Thread dump右边的按钮来获得Thread dump文件
 
windows
在Windows下可以在JVM的console窗口上敲Ctrl-Break。根据不同的设置,thread dump会输出到当前控制台上或应用服务器的日志里。
如果想将日志输出到文件,可以修改tomcat/bin目录下的“catalina.bat”,在文件最后的四个ACTION后增加“>>%CATALINA_BASE%/logs/thread_demp.out”,同时修改bin目录下的startup.bat为:

rem call "%EXECUTABLE%" start "%CMD_LINE_ARGS%" 
call "%EXECUTABLE%" run "%CMD_LINE_ARGS%"

这样就可以将日志输出到logs下的thread_dump.out文件中。也可以下载相应的分析工具对其进行分析。需要说明的一点是,将startup.bat修改为以上内容后,关闭tomcat时,直接关闭DOS窗口就可以了,不用shutdown.bat。

 
 

Thread Information from the Thread Dump File

复制代码
"pool-1-thread-13" prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000] 
java.lang.Thread.State: RUNNABLE 
at java.net.SocketInputStream.socketRead0(Native Method) 
at java.net.SocketInputStream.read(SocketInputStream.java:129) 
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264) 
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) 
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) 
- locked <0x0000000780b7e688> (a java.io.InputStreamReader) 
at java.io.InputStreamReader.read(InputStreamReader.java:167) 
at java.io.BufferedReader.fill(BufferedReader.java:136) 
at java.io.BufferedReader.readLine(BufferedReader.java:299) 
- locked <0x0000000780b7e688> (a java.io.InputStreamReader) 
at java.io.BufferedReader.readLine(BufferedReader.java:362)
复制代码
  * 线程名称:pool-1-thread-13 当使用java.lang.Thread类生成一个线程时,将被命名为Thre-(Number),若java.util.concurrent.ThreadFactory类,将会被命名为pool-(Number)-thread-(Number)

* 优先级: 6,默认是5
* jvm线程id:tid=0x000000000729a000,jvm内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现。)
* 对应系统线程id(NativeThread ID):nid=0x2fb4,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
* 线程状态:runnable
* 起始栈地址:[0x0000000007f0f000]
* Java thread statck trace:这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。 

 

逆向分析:线程堆栈里面的最直观的信息是当前线程的调用上下文,即从哪个函数调用到哪个函数(从下往上看),正执行到哪一类的哪一行。
at java.net.SocketInputStream.read(SocketInputStream.java:129) 
SocketInputStream 类名
read 方法名
SocketInputStream.java 源文件
129 源文件中的行数

在堆栈的第一行信息中,进一步标明了线程在代码级的状态,例如:

java.lang.Thread.State: TIMED_WAITING (parking)

解释如下:

|blocked|

This thread tried to enter asynchronized block, but the lock was taken by another thread. This thread isblocked until the lock gets released.

|blocked (on thin lock)|

This is the same state asblocked, but the lock in question is a thin lock.

|waiting|

This thread calledObject.wait() on an object. The thread will remain there until some otherthread sends a notification to that object.

|sleeping|

This thread calledjava.lang.Thread.sleep().

|parked|

This thread calledjava.util.concurrent.locks.LockSupport.park().

|suspended|

The thread's execution wassuspended by java.lang.Thread.suspend() or a JVMTI agent call.

 

本地线程ID

"NativeThread ID"所指的本地线程是指该java虚拟机所对应的虚拟机中的本地线程,java代码是依附于java虚拟机的本地线程执行的,当启动一个线程时,是创建一个native本地线程,本地线程才是真实的线程实体,为了更加深入理解本地线程和java线程的关系,可以通过以下方式将java虚拟机的本地线程打印出来:
1、试用ps -ef|grep java 获得java进行id
2、试用pstack<java pid> 获得java虚拟机本地线程的堆栈
从操作系统打印出来的虚拟机的本地线程看,本地线程数量和java线程数量是相同的,说明二者是一一对应的关系。
那么本地线程号如何与java线程堆栈文件对应起来呢,每一个线程都有tid,nid的属性。nid即为dump线程的nid。

线程锁解读

线程栈中包含直接信息为:线程个数,每个线程调用的方法堆栈,当前锁的状态。从线程个数可以直接数出来,线程调用的方法堆栈,从下向上看,表示了当前线程调用哪个类哪个方法,锁的状态看起来需要一些技巧,与锁相关的重要信息如下:

  • 当一个线程占有一个锁的时候,线程堆栈会打印一个-locked<0x22bffb60>

  • 当一个线程正在等在其他线程释放该锁,线程堆栈会打印一个-waiting to lock<0x22bffb60>

  • 当一个线程占有一个锁,但又执行在该锁的wait上,线程堆栈中首先打印locked,然后打印-waiting on <0x22c03c60>

在线程堆栈中与锁相关的三个最重要的特征字:locked,waiting to lock,waiting on 了解这三个特征字,就可以对锁进行分析了。

一般情况下,当一个或一些线程正在等待一个锁的时候,应该有一个线程占用了这个锁,即如果有一个线程正在等待一个锁,该锁必然被另一个线程占用,从线程堆栈中看,如果看到waiting to lock<0x22bffb60>,应该也应该有locked<0x22bffb60>,大多数情况下确实如此,但是有些情况下,会发现线程堆栈中可能根本没有locked<0x22bffb60>,而只有waiting to ,这是什么原因呢,实际上,在一个线程释放锁和另一个线程被唤醒之间有一个时间窗,如果这个期间,恰好打印堆栈信息,那么只会找到waiting to ,但是找不到locked 该锁的线程,当然不同的JAVA虚拟机有不同的实现策略,不一定会立刻响应请求,也许会等待正在执行的线程执行完成。

 

thread dump模式的类型
当无法获得一个锁(阻塞)
当一个线程占领住锁而其他线程无法获得这个锁,而导致应用程序所有的性能都下降。在下面的例子中,
BLOCKED_TEST pool-1-thread-1 线程运行时获得<0x0000000780a000b0>锁, 同时BLOCKED_TEST pool-1-thread-2 和 BLOCKED_TEST pool-1-thread-3正在等待获得<0x0000000780a000b0>锁
分析
 
复制代码
"BLOCKED_TEST pool-1-thread-1" prio=6 tid=0x0000000006904800 nid=0x28f4 runnable [0x000000000785f000] 
java.lang.Thread.State: RUNNABLE 
at java.io.FileOutputStream.writeBytes(Native Method) 
at java.io.FileOutputStream.write(FileOutputStream.java:282) 
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:65) 
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:123) 
- locked <0x0000000780a31778> (a java.io.BufferedOutputStream) 
at java.io.PrintStream.write(PrintStream.java:432) 
- locked <0x0000000780a04118> (a java.io.PrintStream) 
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:202) 
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:272) 
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:85) 
- locked <0x0000000780a040c0> (a java.io.OutputStreamWriter) 
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:168) 
at java.io.PrintStream.newLine(PrintStream.java:496) 
- locked <0x0000000780a04118> (a java.io.PrintStream) 
at java.io.PrintStream.println(PrintStream.java:687) 
- locked <0x0000000780a04118> (a java.io.PrintStream) 
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:44) 
- locked <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) 
at com.nbp.theplatform.threaddump.ThreadBlockedState$1.run(ThreadBlockedState.java:7) 
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) 
at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: 
- <0x0000000780a31758> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) 

"BLOCKED_TEST pool-1-thread-2" prio=6 tid=0x0000000007673800 nid=0x260c waiting for monitor entry [0x0000000008abf000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:43) 
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) 
at com.nbp.theplatform.threaddump.ThreadBlockedState$2.run(ThreadBlockedState.java:26) 
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) 
at java.lang.Thread.run(Thread.java:662) 
Locked ownable synchronizers: - <0x0000000780b0c6a0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) 

"BLOCKED_TEST pool-1-thread-3" prio=6 tid=0x00000000074f5800 nid=0x1994 waiting for monitor entry [0x0000000008bbf000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:42) 
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) 
at com.nbp.theplatform.threaddump.ThreadBlockedState$3.run(ThreadBlockedState.java:34) 
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) 
at java.lang.Thread.run(Thread.java:662) 
Locked ownable synchronizers: - <0x0000000780b0e1b8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
复制代码

 

当是死锁的状态
线程A需要获得线程B的锁才能继续执行任务,同时线程B需要获得线程A的锁才能继续执行任务。在Thread Dump中,可以发现 DEADLOCK_TEST-1 线程拥有0x00000007d58f5e48锁,并且试着去获取0x00000007d58f5e60这把锁。另外  DEADLOCK_TEST-2 线程拥有0x00000007d58f5e60锁,并且尝试获取0x00000007d58f5e78锁。,DEADLOCK_TEST-3 线程拥有0x00000007d58f5e78锁,并且尝试获得0x00000007d58f5e48锁。可以看得出来,每个线程都在等待另外线程的锁,这种状态知道一个线程放弃锁之前都不会被改变。
分析

复制代码
"DEADLOCK_TEST-1" daemon prio=6 tid=0x000000000690f800 nid=0x1820 waiting for monitor entry [0x000000000805f000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) 
- waiting to lock <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) 
- locked <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) 
Locked ownable synchronizers: - None 

"DEADLOCK_TEST-2" daemon prio=6 tid=0x0000000006858800 nid=0x17b8 waiting for monitor entry [0x000000000815f000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) 
- waiting to lock <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) 
- locked <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) 
Locked ownable synchronizers: - None 

"DEADLOCK_TEST-3" daemon prio=6 tid=0x0000000006859000 nid=0x25dc waiting for monitor entry [0x000000000825f000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) 
- waiting to lock <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) 
- locked <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) 
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) 
Locked ownable synchronizers: - None
复制代码

 

持续等待来自远程服务的信息
线程看起来是正常的,因为它的状态一直都是RUNNABLE,然而当将thread dump按时间有序的排列,可以看出来socketReadThread线程一直在读socket

分析
复制代码
"socketReadThread" prio=6 tid=0x0000000006a0d800 nid=0x1b40 runnable [0x00000000089ef000] 
java.lang.Thread.State: RUNNABLE 
at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:
129) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) - locked <0x00000007d78a2230> (a java.io.InputStreamReader) at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:107) - locked <0x00000007d78a2230> (a java.io.InputStreamReader) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:93) at java.io.InputStreamReader.read(InputStreamReader.java:151) at com.nbp.theplatform.threaddump.ThreadSocketReadState$1.run(ThreadSocketReadState.java:27) at java.lang.Thread.run(Thread.java:662)
复制代码

 

当等待
线程保持wait状态,在thread dump中,ioWaitThread线程等待从LinkedBlockingQueue中接收信息,如果LinkedBlockQueue一直没有信息,那么线程的状态将不会改变。

Dump
复制代码
"IoWaitThread" prio=6 tid=0x0000000007334800 nid=0x2b3c waiting on condition [0x000000000893f000] 
java.lang.Thread.State: WAITING (parking) 
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007d5c45850> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987) at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:440) at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:629) at com.nbp.theplatform.threaddump.ThreadIoWaitState$IoWaitHandler2.run(ThreadIoWaitState.java:89) at java.lang.Thread.run(Thread.java:662)
复制代码

 

当线程资源不能正常组织
当线程的资源无法正常组织,不用的线程将会堆积起来。如果发生了,建议监听下线程组织的过程或者检查线程终止的条件

Thread
 

如何利用Thread Dump解决问题

使用方案

现象1:cpu高,load高,响应很慢

方案:
* 一个请求过程中多次dump

* 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了。

 

现象2:查找占用cpu最多的线程信息

方案:
* 使用命令: top -H -p pid(pid为被测系统的进程号),找到导致cpu高的线程id。

上述Top命令找到的线程id,对应着dump thread信息中线程的nid,只不过一个是十进制,一个是十六进制。

* 在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息。

* 多次dump,比较方法调用

 

现象3:cpu使用率不高但是响应时间很长

方案:
* 进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因。

1)Waiting on condition:等待某个资源或条件发生来唤醒自己。比如线程正在sleep,网络读写繁忙而等待。

2)Blocked:阻塞,waiting for monitor entry的线程。

 

现象4:请求无法响应

方案:
* 多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,锁住了!

 

热锁

热锁,也往往是导致系统性能瓶颈的主要因素。其表现特征为:由于多个线程对临界区,或者锁的竞争,可能出现:

  • 频繁的线程的上下文切换:从操作系统对线程的调度来看,当线程在等待资源而阻塞的时候,操作系统会将之切换出来,放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
  • 大量的系统调用:因为线程的上下文切换,以及热锁的竞争,或者临界区的频繁的进出,都可能导致大量的系统调用。
  • 大部分CPU开销用在“系统态 ”:线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,虽然系统很忙碌,但是 CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。 
  • 随着 CPU数目的增多,系统的性能反而下降。因为CPU数目多,同时运行的线程就越多,可能就会造成更频繁的线程上下文切换和系统态的CPU开销,从而导致更糟糕的性能。 

    上面的描述,都是一个 scalability(可扩展性)很差的系统的表现。从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。
    那么,怎么去了解 “热锁 ”出现在什么地方呢?一个重要的方法还是结合操作系统的各种工具观察系统资源使用状况,以及收集Java线程的DUMP信息,看线程都阻塞在什么方法上,了解原因,才能找到对应的解决方法。
    我们曾经遇到过这样的例子,程序运行时,出现了以上指出的各种现象,通过观察操作系统的资源使用统计信息,以及线程 DUMP信息,确定了程序中热锁的存在,并发现大多数的线程状态都是 Waitingfor monitor entry或者 Wait on monitor,且是阻塞在压缩和解压缩的方法上。后来采用第三方的压缩包 javalib替代 JDK自带的压缩包后,系统的性能提高了几倍。

 

 

例子1:当cpu使用率不正常的偏高,查找占用CPU最多的线程信息
   
  1 )提取最高cpu使用率的线程
$ ps -mo pid.lwp.stime.time.cpu -C java  
PID LWP STIME TIME %CPU
10029 Dec07 00:02:02 99.5 -
10039 Dec07 00:00:00 0.1 -
10040 Dec07 00:00:00 95.5 -

     从应用中找出cpu使用率的线程,获得轻量级锁使用的cpu率,并将LWP对应的数字(10039) 转换成十六进制(0x2737) 

    2 )获得thread dump后,检测线程的状态
    提取出应用中thread dump中pid为10029 ,再找出nid为0x2737的线程
复制代码
"NioProcessor-2" prio=10 tid=0x0a8d2800 nid=0x2737 runnable [0x49aa5000] 
java.lang.Thread.State: RUNNABLE 
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) 
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:210) 
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65) 
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69) 
- locked <0x74c52678> (a sun.nio.ch.Util$1) 
- locked <0x74c52668> (a java.util.Collections$UnmodifiableSet) 
- locked <0x74c501b0> (a sun.nio.ch.EPollSelectorImpl) 
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80) 
at external.org.apache.mina.transport.socket.nio.NioProcessor.select(NioProcessor.java:65) 
at external.org.apache.mina.common.AbstractPollingIoProcessor$Worker.run(AbstractPollingIoProcessor.java:708) 
at external.org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:51) 
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) 
at java.lang.Thread.run(Thread.java:662)
复制代码

    每隔一小时提取几次,然后分析线程的状态以确定问题。

 
例2:当性能不正常的下降
    在获得thread dump后,下面一组线程的状态是BLOCKED 
复制代码
" DB-Processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at beans.ConnectionPool.getConnection(ConnectionPool.java:102) 
- waiting to lock <0xe0375410> (a beans.ConnectionPool) 
at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) 
at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) 

"DB-Processor-14" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f020] 
java.lang.Thread.State: BLOCKED (on object monitor) 
at beans.ConnectionPool.getConnection(ConnectionPool.java:102) 
- waiting to lock <0xe0375410> (a beans.ConnectionPool) 
at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) 
at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) 

" DB-Processor-3" daemon prio=5 tid=0x00928248 nid=0x8b waiting for monitor entry [0x000000000825d080] 
java.lang.Thread.State: RUNNABLE 
at oracle.jdbc.driver.OracleConnection.isClosed(OracleConnection.java:570) 
- waiting to lock <0xe03ba2e0> (a oracle.jdbc.driver.OracleConnection) 
at beans.ConnectionPool.getConnection(ConnectionPool.java:112) 
- locked <0xe0386580> (a java.util.Vector) - locked <0xe0375410> (a beans.ConnectionPool) 
at beans.cus.Cue_1700c.GetNationList(Cue_1700c.java:66) 
at org.apache.jsp.cue_1700c_jsp._jspService(cue_1700c_jsp.java:120)
复制代码

If the threads are BLOCKED, extract the threads related to the lock that the threads are trying to obtain.

 
通过thread dump,可以确认处在BLOCKED状态的线程,因为<0xe0375410>锁无法被获得,这种问题可以目前持有锁的堆栈跟踪中解决问题。
在使用dbms时,有两个原因会导致上面描述的问题频繁发生。
第一个原因是:
不适当的配置:尽管这些线程仍然在工作,因为数据库连接池的不适当配置,使得无法显示出最好的性能。通过多次收集分析thread dumps,将会发现一些之前处于BLOCKED状态的线程会处于不同的状态。
第二个原因是:
不正当的连接。如果和数据库的连接保持异常,那么线程会一直等待超时。在这个例子中,通过多次收集分析thread dumps,将会看到和数据库有关的线程会一直保持BLOCKED状态。通过适当的参数,比如超时时间,可以缩短问题发生的时间
 
 
 
简单的Thread Dump编码:
线程的命名
使用java.lang.Thread 类来创建一个线程对象,将被命名为Thread-(Number)。当使用java.util.concurrent.DefaultThreadFactory对象创建线程,将被命名为pool-(Number)-thread-(Number)。在分析应用中几十到几百的线程时,如果所有的线程都使用默认的名字,分析会变得非常困难,很难区别出线程。
因此,可以养成在创建线程时为线程命名的好习惯
通过java.lang.Thread创建线程,可以给线程构造参数中传入自定义的名
public Thread(Runnable target, String name); 
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize); 
通过java.util.concurrent.ThreadFactory创建线程,可以取自己ThredFactory的名。如果不需要特殊的功能,可以使用下面的MyThreadFactory
import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class MyThreadFactory implements ThreadFactory { private static final ConcurrentHashMap<String, AtomicInteger> POOL_NUMBER = new ConcurrentHashMap<String, AtomicInteger>();
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public MyThreadFactory(String threadPoolName) {
if (threadPoolName == null) { throw new NullPointerException("threadPoolName"); }
POOL_NUMBER.putIfAbsent(threadPoolName, new AtomicInteger());
SecurityManager securityManager = System.getSecurityManager();
group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup();
AtomicInteger poolCount = POOL_NUMBER.get(threadPoolName);
if (poolCount == null) { namePrefix = threadPoolName + " pool-00-thread-"; }
else { namePrefix = threadPoolName + " pool-" + poolCount.getAndIncrement() + "-thread-";
}
}
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0);
if (thread.isDaemon()) { thread.setDaemon(false); }
if (thread.getPriority() != Thread.NORM_PRIORITY)
{ thread.setPriority(Thread.NORM_PRIORITY); }
return thread; }
}
 
通过MBean来获得更多的信息
可以通过MBean来获得更多的ThreadInfo的信息。同样使用ThreadInfo也可以获得更多的在thread dumps中难以获取的信息。
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); 
long[] threadIds = mxBean.getAllThreadIds();
ThreadInfo[] threadInfos = mxBean.getThreadInfo(threadIds);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println( threadInfo.getThreadName());
System.out.println( threadInfo.getBlockedCount());
System.out.println( threadInfo.getBlockedTime());
System.out.println( threadInfo.getWaitedCount());
System.out.println( threadInfo.getWaitedTime());
}
使用ThreaInfo中的信息可以获得等待线程或者阻塞线程所用的时间,也可以获得已经被停用了的异常线程的列表
 
 
参考资料:
http://www.cfanz.cn/index.php?c=article&a=read&id=64795
http://java.dzone.com/articles/how-analyze-java-thread-dumps 

另一篇 :https://www.cnblogs.com/kabi/p/5169383.html


猜你喜欢

转载自blog.csdn.net/albertfly/article/details/80484085