jstack线程分析

1,JVM线程

在线程中,有一些 JVM内部的后台线程,来执行譬如垃圾回收,或者低内存的检测等等任务,这些线程往往在 JVM初始化的时候就存在,如下所示: 

        "Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000] 

        "CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798] 

        "Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000] 

        "Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8] 
        at java.lang.Object.wait(Native Method) 
        - waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock) 
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116) 
        - locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock) 
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132) 
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159) 

   "Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38] 
        at java.lang.Object.wait(Native Method) 

        - waiting on <0xef600758> (a java.lang.ref.Reference$Lock) 

        at java.lang.Object.wait(Object.java:474) 

        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116) 

        - locked <0xef600758> (a java.lang.ref.Reference$Lock) 

        "VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable 

        "VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition 

下面把一些比较常见的JVM内部运行线程列出来。

线程 所属 说明
Attach Listener JVM           Attach Listener线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。
Signal Dispatcher JVM         前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。
Compiler Thread0 JVM        用来调用JITing,实时编译装卸class。通常,jvm会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,例如:CompilerThread1
Concurrent Mark-Sweep GC Thread JVM 并发标记清除垃圾回收器(就是通常所说的CMS GC)线程,该线程主要针对于老年代垃圾回收。ps:启用该垃圾回收器,需要在jvm启动参数中加上:-XX:+UseConcMarkSweepGC
Destroy JavaVM JVM 执行main()的线程在main执行完后调用JNI中的jni_DestroyJavaVM()方法唤起DestroyJavaVM线程。   JVM在Jboss服务器启动之后,就会唤起DestroyJavaVM线程,处于等待状态,等待其它线程(java线程和native线程)退出时通知它卸载JVM。线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM线程卸载JVM。ps:扩展一下:1.如果线程退出时判断自己不为最后一个非deamon线程,那么调用thread->exit(false),并在其中抛出thread_end事件,jvm不退出。2.如果线程退出时判断自己为最后一个非deamon线程,那么调用before_exit()方法,抛出两个事件: 事件1:thread_end线程结束事件、事件2:VM的death事件。然后调用thread->exit(true)方法,接下来把线程从active list卸下,删除线程等等一系列工作执行完成后,则通知正在等待的DestroyJavaVM线程执行卸载JVM操作。
Dispatcher-Thread-3 Log4j       Log4j具有异步打印日志的功能,需要异步打印日志的Appender都需要注册到AsyncAppender对象里面去,由AsyncAppender进行监听,决定何时触发日志打印操作。AsyncAppender如果监听到它管辖范围内的Appender有打印日志的操作,则给这个Appender生成一个相应的event,并将该event保存在一个buffuer区域内。  Dispatcher-Thread-3线程负责判断这个event缓存区是否已经满了,如果已经满了,则将缓存区内的所有event分发到Appender容器里面去,那些注册上来的Appender收到自己的event后,则开始处理自己的日志打印工作。Dispatcher-Thread-3线程是一个守护线程。
Finalizer线程 JVM 这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:1)只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;2)该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;3) JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;4) JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难;
Gang worker#0 JVM JVM用于做新生代垃圾回收(monir gc)的一个线程。#号后面是线程编号,例如:Gang worker#1
GC Daemon JVM GC Daemon线程是JVM为RMI提供远程分布式GC使用的,GC Daemon线程里面会主动调用System.gc()方法,对服务器进行Full GC。 其初衷是当RMI服务器返回一个对象到其客户机(远程方法的调用方)时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。不过,我们现在jvm启动参数都加上了-XX:+DisableExplicitGC配置,所以,这个线程只有打酱油的份了。
Java2D Disposer JVM          这个线程主要服务于awt的各个组件。说起该线程的主要工作职责前,需要先介绍一下Disposer类是干嘛的。Disposer提供一个addRecord方法。如果你想在一个对象被销毁前再做一些善后工作,那么,你可以调用Disposer#addRecord方法,将这个对象和一个自定义的DisposerRecord接口实现类,一起传入进去,进行注册。Disposer类会唤起“Java2D Disposer”线程,该线程会扫描已注册的这些对象是否要被回收了,如果是,则调用该对象对应的DisposerRecord实现类里面的dispose方法。Disposer实际上不限于在awt应用场景,只是awt里面的很多组件需要访问很多操作系统资源,所以,这些组件在被回收时,需要先释放这些资源。
java.util.concurrent.ThreadPoolExecutor$Worker JVM  
JDWP Event Helper Thread JVM JDWP是通讯交互协议,它定义了调试器和被调试程序之间传递信息的格式。它详细完整地定义了请求命令、回应数据和错误代码,保证了前端和后端的JVMTI和JDI的通信通畅。 该线程主要负责将JDI事件映射成JVMTI信号,以达到调试过程中操作JVM的目的。
JDWP Transport Listener: dt_socket JVM  该线程是一个Java Debugger的监听器线程,负责受理客户端的debug请求。通常我们习惯将它的监听端口设置为8787。
Low Memory Detector JVM 这个线程是负责对可使用内存进行检测,如果发现可用内存低,分配新的内存空间。
process reaper JVM    该线程负责去执行一个OS命令行的操作。
Reference Handler JVM         JVM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
SurrogateLockerThread(CMS) JVM           这个线程主要用于配合CMS垃圾回收器使用,它是一个守护线程,其主要负责处理GC过程中,Java层的Reference(指软引用、弱引用等等)与jvm内部层面的对象状态同步。这里对它们的实现稍微做一下介绍:这里拿WeakHashMap做例子,将一些关键点先列出来(我们后面会将这些关键点全部串起来):1.      我们知道HashMap用Entry[]数组来存储数据的,WeakHashMap也不例外,内部有一个Entry[]数组。
2.       WeakHashMap的Entry比较特殊,它的继承体系结构为Entry->WeakReference->Reference。
3.      Reference里面有一个全局锁对象:Lock,它也被称为pending_lock.   注意:它是静态对象。
4.      Reference 里面有一个静态变量:pending。
5. Reference 里面有一个静态内部类:ReferenceHandler的线程,它在static块里面被初始化并且启动,启动完成后处于wait状态,它在一个Lock同步锁模块中等待。
6.      另外,WeakHashMap里面还实例化了一个ReferenceQueue列队,这个列队的作用,后面会提到。
7.      上面关键点就介绍完毕了,下面我们把他们串起来。
    假设,WeakHashMap对象里面已经保存了很多对象的引用。JVM在进行CMS GC的时候,会创建一个ConcurrentMarkSweepThread(简称CMST)线程去进行GC,ConcurrentMarkSweepThread线程被创建的同时会创建一个SurrogateLockerThread(简称SLT)线程并且启动它,SLT启动之后,处于等待阶段。CMST开始GC时,会发一个消息给SLT让它去获取Java层Reference对象的全局锁:Lock。直到CMS GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference的pending属性当中(每次GC完毕之后,pending属性基本上都不会为null了),然后通知SLT释放并且notify全局锁:Lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。当我们下次从WeakHashMap对象里面get、put数据或者调用size方法的时候,WeakHashMap就会将ReferenceQueue列队中的WeakReference依依poll出来去和Entry[]数据做比较,如果发现相同的,则说明这个Entry所保存的对象已经被GC掉了,那么将Entry[]内的Entry对象剔除掉。
taskObjectTimerFactory JVM          顾名思义,该线程就是用来执行任务的。当我们把一个任务交给Timer对象,并且告诉它执行时间,周期时间后,Timer就会将该任务放入任务列队,并且通知taskObjectTimerFactory线程去处理任务,taskObjectTimerFactory线程会将状态为取消的任务从任务列队中移除,如果任务是非重复执行类型的,则在执行完该任务后,将它从任务列队中移除,如果该任务是需要重复执行的,则计算出它下一次执行的时间点。
VM Periodic Task Thread JVM        该线程是JVM周期性任务调度的线程,它由WatcherThread创建,是一个单例对象。该线程在JVM内使用得比较频繁,比如:定期的内存监控、JVM运行状况监控,还有我们经常需要去执行一些jstat这类命令查看gc的情况,如下:jstat -gcutil 23483 250 7  这个命令告诉jvm在控制台打印PID为:23483的gc情况,间隔250毫秒打印一次,一共打印7次。
VM Thread JVM         这个线程就比较牛b了,是jvm里面的线程母体,根据hotspot源码(vmThread.hpp)里面的注释,它是一个单例的对象(最原始的线程)会产生或触发所有其他的线程,这个单个的VM线程是会被其他线程所使用来做一些VM操作(如,清扫垃圾等)。在 VMThread的结构体里有一个VMOperationQueue列队,所有的VM线程操作(vm_operation)都会被保存到这个列队当中,VMThread本身就是一个线程,它的线程负责执行一个自轮询的loop函数(具体可以参考:VMThread.cpp里面的void VMThread::loop()),该loop函数从VMOperationQueue列队中按照优先级取出当前需要执行的操作对象(VM_Operation),并且调用VM_Operation->evaluate函数去执行该操作类型本身的业务逻辑。ps:VM操作类型被定义在vm_operations.hpp文件内,列举几个:ThreadStop、ThreadDump、PrintThreads、GenCollectFull、GenCollectFullConcurrent、CMS_Initial_Mark、CMS_Final_Remark…..有兴趣的同学,可以自己去查看源文件。

 我们更多的是要观察用户级别的线程,如下所示:

 

        "Thread-1" prio=10 tid=0x08223860 nid=0xa waiting on condition [0xef47a000..0xef47ac38]
        at java.lang.Thread.sleep(Native Method)
        at testthread.MySleepingThread.method2(MySleepingThread.java:53)
        - locked <0xef63d600> (a testthread.MySleepingThread)
        at testthread.MySleepingThread.run(MySleepingThread.java:35)
        at java.lang.Thread.run(Thread.java:595) 

        我们能看到: 

  • 线程的状态: waiting on condition
  • 线程的调用栈
  • 线程的当前锁住的资源: <0xef63d600>

        这些信息对我们随后的分析都有用处。 

2,JVM线程状态分析

线程的状态是一个重要的指标,那么线程常见的有哪些状态呢?线程在什么样的情况下会进入这种状态呢?我们能从中发现什么线索?

2.1 runnable 

该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。 

2.2 waiting on condition 

该状态出现在线程等待某个条件的发生。比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。

另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。

2.3 waiting for monitor entry 和 in Object.wait()

在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段。

我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。

我们使用 Monitor Object 设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。只要把类的所有对象方法都用synchronized关键字修饰,并且所有域都为私有(也就是只能通过方法访问对象状态),就是一个货真价实的Monitor了。

下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图: 
  
从图中可以看出,每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。 

我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。

这时有两种可能性: 

1,该 monitor不被其它线程拥有, Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码 
2,该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。 
在第一种情况下,线程将处于 “Runnable”的状态,而第二种情况下,线程会显示处于 “waiting for monitor entry”。

当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll() , “ Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的 Monitor,恢复到运行态。在 “Wait Set”中的线程表现为: in Object.wait()。

3,分析java线程

为了可以理解/分析线程转储,首先要理解线程转储的各个部分。让我们先拿一个简单的线程堆栈为例,并且去了解他的每个部分。

"pool-1-thread-13" daemon prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000] java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)

pool-1-thread-13 线程的名字daemon 表明这个线程是一个守护线程

prio=6 线程的优先级

tid Java的线程Id (这个线程在当前虚拟机中的唯一标识).

nid 线程本地标识,也就是线程在操作系统中的标识

runnable 线程的状态 (参考上面的)

[x..y] 当前运行的线程在堆中的地址范围

java.lang.Thread.State: RUNNABLE 线程在它从创建到消亡所处的阶段。按照JDK中的解释,一个线程从创建到消亡的阶段分为六种:
(1)NEW:线程刚被创建,但是还没有启动;
(2)RUNNABLE:正在JVM中被运行的线程的状态,有可能因为缺少CPU等资源进入等待状态;
(3)BLOCKED:阻塞状态,等待其他线程释放同步锁或者IO;
(4)WAITING:当线程调用了wait方法(无参)、join方法(无参)、LockSupport.park方法之后,进入等待WAITING状态,等待被唤醒;
(5)TIMED_WAITING:当线程调用了sleep方法、wait方法(有参)、join方法(有参)、LockSupport.parkNanos、LockSupport.parkUntil;
(6)TERMINATED:结束执行;

比如在下面这个示例中,是个较为典型的死锁情况: 

"Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000

..0x02d3fd68]

at deadlockthreads.TestThread.run(TestThread.java:31)

- waiting to lock <0x22c19f18> (a java.lang.Object)

- locked <0x22c19f20> (a java.lang.Object) 
"Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000

..0x02cff9e8]

at deadlockthreads.TestThread.run(TestThread.java:31)

- waiting to lock <0x22c19f20> (a java.lang.Object)

- locked <0x22c19f18> (a java.lang.Object) 

线程”Thread-1“获得了锁<0x22c19f20>,等待锁<0x22c19f18>;

而线程"Thread-0"获得了锁<0x22c19f18>,等待锁<0x22c19f20>。


同事总结的,非常不错,发表出来共赏。

你可能感兴趣的:(jstack线程分析)