0%

Java栈分析与调优

JStack

寻找问题线程的过程

1
top -Hp  1704

![top thread](_v_images/20190728100942815_354006932.png =690x)

  1. 目标线程的id转换为16进制
  2. jstack dump线程栈
1
2
3
4
5
6
7
8
9
10
11
[deploy@centos ~]$ printf '%x' 1721
6b9
[deploy@centos ~]$ jstack -l 1704 | grep 6b9 -A 20
"handler-0" #9 prio=5 os_prio=0 tid=0x00007fa21c14e000 nid=0x6b9 waiting on condition [0x00007fa1f2884000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.lk.optimization.demo.Worker.run(DemoTest.java:34)
at java.lang.Thread.run(Thread.java:748)

Locked ownable synchronizers:
- None
1
2
3
4
5
6
jstack [ option ] pid
基本参数:
-F 当’jstack [-l] pid’没有响应的时候强制打印栈信息
-l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.
-m 打印java和native c/c++框架的所有栈信息. -h | -help打印帮助信息
pid 需要被打印配置信息的java进程id,可以用jps工具查询.
1
kill -3 <pid>

线程的状态

RUNNABLE

为了把runnable打出来,写了个死循环

1
2
3
4
5
6
7
8
9
10
11
12
new Thread(){
public void run(){
int i=0;
while(true){
i++;
if(i>100000000){
i=0;
System.out.println("hha");
}
}
}
}.start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"Thread-0" #19 prio=5 os_prio=0 tid=0x00007f2810152000 nid=0x7aa runnable [0x00007f27f8a73000]
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
- locked <0x00000000e0c1d7e8> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x00000000e0c02988> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
- locked <0x00000000e0c02940> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.newLine(PrintStream.java:546)
- eliminated <0x00000000e0c02988> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:807)
- locked <0x00000000e0c02988> (a java.io.PrintStream)
at com.lk.optimization.demo.DemoTest$1.run(DemoTest.java:14)

Locked ownable synchronizers:
- None

TIMED_WAITING

1
2
3
4
5
6
7
8
9
10
11
12
class Worker implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
"handler-0" #9 prio=5 os_prio=0 tid=0x00007fa21c14e000 nid=0x6b9 waiting on condition [0x00007fa1f2884000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.lk.optimization.demo.Worker.run(DemoTest.java:34)
at java.lang.Thread.run(Thread.java:748)

Locked ownable synchronizers:
- None

JVM线程对应的系统线程, 16进制的
正等待<0x00007fa1f2884000>

Blocked

多线程竞争synchronized锁

![stack-blocked](_v_images/20190728105849426_741235393.png =778x)

很明显:线程1获取到锁,处于RUNNABLE状态,线程2处于BLOCK状态
1、locked <0x000000076bf62208>说明线程1对地址为0x000000076bf62208对象进行了加锁;
2、waiting to lock <0x000000076bf62208> 说明线程2在等待地址为0x000000076bf62208对象上的锁;
3、waiting for monitor entry [0x000000001e21f000]说明线程1是通过synchronized关键字进入了监视器的临界区,并处于”Entry Set”队列,等待monitor,具体实现可以参考深入分析synchronized的JVM实现

通过wait挂起线程

1
2
3
4
5
6
7
8
9
10
11
12
13
static class Task implements Runnable {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
//TimeUnit.SECONDS.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

![stack wait](_v_images/20190728110132556_948612836.png =778x)

线程1和2都处于WAITING状态
1、线程1和2都是先locked <0x000000076bf62500>,再waiting on <0x000000076bf62500>,之所以先锁再等同一个对象,是因为wait方法需要先通过synchronized获得该地址对象的monitor;
2、waiting on <0x000000076bf62500>说明线程执行了wait方法之后,释放了monitor,进入到”Wait Set”队列,等待其它线程执行地址为0x000000076bf62500对象的notify方法,并唤醒自己,具体实现可以参考深入分析Object.wait/notify实现机制

Wait on condition

该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。在 Java引入 NIO之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 NIO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。

如果发现有大量的线程都在处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ; 观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;如果程序运行在 Solaris 10平台上,可以用 dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。

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

Waitingfor monitor entry 和 in Object.wait()

在多线程的 JAVA程序中,实现线程之间的同步,就要说说Monitor。Monitor是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图

a Java monitor

从图中可以看出,每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitorentry”,而在 “Wait Set”中等待的线程状态是“in Object.wait()”。

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

1
2
3
4
5
synchronized(obj){ 

.........

}

这时有两种可能性:

  • 该 monitor不被其它线程拥有,Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码 。此时线程将处于Runnable状态;

  • 该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。此时dump的信息显示“waiting for monitor entry”。

1
2
3
4
5
6
"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8] 

at testthread.WaitThread.run(WaitThread.java:39)
- waiting to lock <0xef63bf08> (a java.lang.Object)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)

临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这和我们多线程的程序的初衷是相反的。如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38] 

at java.lang.Object.wait(Native Method)

- waiting on <0xef63beb8> (a java.util.ArrayList)

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

at testthread.MyWaitThread.run(MyWaitThread.java:40)

- locked <0xef63beb8> (a java.util.ArrayList)

at java.lang.Thread.run(Thread.java:595)

仔细观察上面的 DUMP信息,你会发现它有以下两行:

² locked <0xef63beb8> (ajava.util.ArrayList)

² waiting on <0xef63beb8> (ajava.util.ArrayList)

这里需要解释一下,为什么先 lock了这个对象,然后又 waiting on同一个对象呢?让我们看看这个线程对应的代码:

1
2
3
4
5
6
7
8
9
synchronized(obj){

.........

obj.wait();

.........

}

线程的执行中,先用 synchronized 获得了这个对象的 Monitor(对应于 locked <0xef63beb8> )。当执行到 obj.wait(), 线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on<0xef63beb8> )。

往在你的程序中,会出现多个类似的线程,他们都有相似的 dump也可能是正常的。比如,在程序中有多个服务线程,设计成从一个队列里面读取请求数据。这个队列就是 lock以及 waiting on的对象。当队列为空的时候,这些线程都会在这个队列上等待,直到队列有了数据,这些线程被notify,当然只有一个线程获得了 lock,继续执行,而其它线程继续等待。

JVM的一些重要线程

Attach Listener

JVM

Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。 如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。

Signal Dispatcher

JVM

前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。 signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。

CompilerThread0

JVM

用来调用JITing,实时编译装卸class 。 通常,jvm会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,例如:CompilerThread1

Concurrent Mark-Sweep GC Thread

JVM

并发标记清除垃圾回收器(就是通常所说的CMS GC)线程, 该线程主要针对于老年代垃圾回收。ps:启用该垃圾回收器,需要在jvm启动参数中加上: -XX:+UseConcMarkSweepGC

DestroyJavaVM

JVM

执行main()的线程在main执行完后调用JNI中的 jni_DestroyJavaVM() 方法唤起DestroyJavaVM 线程。

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里面的很多组件需要访问很多操作系统资源,所以,这些组件在被回收时,需要先释放这些资源。

InsttoolCacheScheduler_QuartzSchedulerThread

Quartz

InsttoolCacheScheduler_QuartzSchedulerThread是Quartz的主线程,它主要负责实时的获取下一个时间点要触发的触发器,然后执行触发器相关联的作业 。

原理大致如下:

Spring和Quartz结合使用的场景下,Spring IOC容器初始化时会创建并初始化Quartz线程池(TreadPool),并启动它。刚启动时线程池中每个线程都处于等待状态,等待外界给他分配Runnable(持有作业对象的线程)。

继而接着初始化并启动Quartz的主线程

(InsttoolCacheScheduler_QuartzSchedulerThread),该线程自启动后就会处于等待状态。等待外界给出工作信号之后,该主线程的run方法才实质上开始工作。run中会获取JobStore中下一次要触发的作业,拿到之后会一直等待到该作业的真正触发时间,然后将该作业包装成一个JobRunShell对象(该对象实现了Runnable接口,其实看是上面TreadPool中等待外界分配给他的Runnable),然后将刚创建的JobRunShell交给线程池,由线程池负责执行作业。

线程池收到Runnable后,从线程池一个线程启动Runnable,反射调用JobRunShell中的run方法,run方法执行完成之后, TreadPool将该线程回收至空闲线程中。

InsttoolCacheScheduler_Worker-2

Quartz

InsttoolCacheScheduler_Worker-2线程就是ThreadPool线程的一个简单实现,它主要负责分配线程资源去执行

InsttoolCacheScheduler_QuartzSchedulerThread线程交给它的调度任务(也就是JobRunShell)。

JBossLifeThread

Jboss

Jboss主线程启动成功,应用程序部署完毕之后将JBossLifeThread线程实例化并且start,JBossLifeThread线程启动成功之后就处于等待状态,以保持Jboss Java进程处于存活中。 所得比较通俗一点,就是Jboss启动流程执行完毕之后,为什么没有结束? 就是因为有这个线程hold主了它。

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,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题 。

Surrogate Locker Thread (CMS)

JVM

这个线程主要用于配合CMS垃圾回收器使用,它是一个守护线程,其主要负责处理GC过程中,Java层的Reference(指软引用、弱引用等等)与jvm 内部层面的对象状态同步。 这里对它们的实现稍微做一下介绍:这里拿 WeakHashMap做例子,将一些关键点先列出来(我们后面会将这些关键点全部串起来):

1.我们知道HashMap用Entry[]数组来存储数据的,WeakHashMap也不例外, 内部有一个Entry[]数组。

  1. WeakHashMap的Entry比较特殊,它的继承体系结构为

Entry->WeakReference->Reference 。

3.Reference 里面有一个全局锁对象:Lock,

它也被称为pending_lock.注意:它是静态对象。

  1. Reference 里面有一个静态变量:pending。

  2. 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…..