本系列的前面四篇文件从面到点,逐层介绍了如果分析生产环境Java进程的运行状态,其中第一篇jps介绍了如何找到所需要的Java进程,第二篇jinfo介绍了如何查看Java进程的配置信息,第三篇jmap介绍了如何查看Java内存对象信息,以及堆内存信息,第四篇jstat介绍了如何分析Java进程的GC信息和JIT信息。本文将承接上文内容,利用jstack工具深入分析Java进程内的线程详细信息。

jstack介绍

jstack主要是用来分析Java进程中的Java线程的方法调用堆栈。它的目标是定位线程长时间停顿的原因(如:线程死锁,线程死循环,线程被IO占用)。当你的程序出现以上问题,或者以上的近似问题的时候,你就可以调用jstack查看各个线程的调用堆栈,分析线程具体在做什么,资源消耗在哪里。从而可以知道问题的原因,采取相应的补救措施。就我在公司使用jstack的场景来看,基本上都是使用jstack来分析线程的死锁问题。所以下文将大体介绍一下Java线程的锁。具体的理论知识可以参考我前面的文章:Java锁分析

java线程介绍

java线程的几种状态

要想用jtack查看分析线程的锁信息,首先必须知道Java线程的几种状态。

  • NEW 线程未启动,一般jstack中不会出现这种未运行的线程
  • RUNNABLE 线程正在执行,这是一种正常的状态
  • BLOCKED 线程等待获取锁,目前线程被阻塞
  • WATING 线程主动放弃锁,然后在条件队列中,等待信息重新获取锁。请看前文如何理解Condition
  • TIMED_WATING 较上一个状态添加了一个等待时间
  • TERMINATED 线程已经执行完成

观察上面的几种线程的状态我们发现,我们首要关注的是:BLOCKED,WATING两种状态。看到这两种状态一定要小心,很可能就是线程死锁的原因。

jstack关注的几个关键字

locked

locked <地址> 表示当前线程获取锁成功,锁的地址为 <地址>

waiting to lock

waiting to lock <地址> 表示当前线程等待获取锁,等待的锁的地址为<地址>

waiting on

waiting on <地址> 表示当前线程获取锁成功之后,主动释放锁,然后在条件队列中等待信号重新去获取锁

parking to wait for

jstack使用教程


查看改进程内的所有线程堆栈
jstack pid
查看某个线程的堆栈
在实际的问题中,往往是Java进程内的一个线程出现了问题需要查看线程的堆栈,而不是直接查看当前Java进程的所有线程的堆栈。因为这样更具有针对性。那么可以采取如下的方法:
1 执行 top -p 9968 -H 找到Java进程内的线程的列表
2 执行 top 9972 查看线程的堆栈,这里9972是Java进程9968中的线程。

实战分析

运行一下的Java程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.qunar.des.baofang;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by guochenglai on 7/29/16.
*/
public class JStackTest {
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
new Thread(new DeadLockTest(true)).start();
new Thread(new DeadLockTest(false)).start();
}
static class DeadLockTest implements Runnable {
private boolean flag;
public DeadLockTest(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (true) {
if (flag) {
try {
lock1.lock();
System.out.println("lock1 :"+Thread.currentThread().getName());
lock2.lock();
System.out.println("lock2 :" + Thread.currentThread().getName());
}finally {
lock1.unlock();
lock2.unlock();
}
} else {
try {
lock2.lock();
System.out.println("lock2 :" + Thread.currentThread().getName());
lock1.lock();
System.out.println("lock1 :"+Thread.currentThread().getName());
}finally {
lock2.unlock();
lock1.unlock();
}
}
}
}
}
}

执行 jsp -l 找到Java进程

执行 jstack 25817 分析Java的线程堆栈

由图可以看到线程“Thread-1”的线程的地址为“0x000000076b29b578”锁使用者的地址为“0x000000076b29b548”,“Thread-0”线程的地址为“0x000000076b29b548”锁使用者的地址为“0x000000076b29b578”,因此两个线程相互等待对方的锁,也就造成了死问题。

分析Java线程的脚本

以下的一个脚本是我在公司分析Java线程堆栈用的。现在提供出来供大家学习交流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/bin/sh
JAVA_HOME=/home/q/java/default
WEB_HOME=/home/q/www
jstack="$JAVA_HOME/bin/jstack"
processId=$1
#如果没有输入进程ID,提示用户输入
if [ ! -n "$processId" ] ;then
echo "please input a java processId "
read processId
fi
echo $processId
#如果是标准输出则变成红色,否则正常输出
redEcho(){
if [ -c /dev/stdout ] ; then
echo -e "\033[1;31m$@\033[0m"
else
echo "$@"
fi
}
showProcessAllthreads(){
while read eachLine ; do
threadId=`echo $eachLine | awk '{print $1}'`
threadId0x=`printf %x ${threadId}`
pcpu=`echo ${eachLine} | awk '{print $2}'`
totalTime=`echo ${eachLine} | awk '{print $3}'`
#存储文件
jstackFile=/tmp/${uniqueId}_${processId}
sudo -u tomcat $jstack ${processId} > ${jstackFile} || {
redEcho "failed to use jstack commond on java process ${processId}"
rm -rf ${jstackFile}
continue
}
redEcho "java processId is [${processId}] : current thread is(${threadId}/${threadId0x}) : cost total process time is [${totalTime}] : use total cup precent is [${pcpu}]"
sed "/nid=0x${threadId0x}/,/^$/p" -n ${jstackFile}
done
}
#为当前的程序的文件生成一个唯一的地址(时间+随机数+进程号(注意这个进程号是当前程序的进程号,不是java城市的进程号))
uniqueId=`date +%s`_${RANDOM}_$$
echo $uniqueId
top -p $processId -H -n 1 -b | sed -n "8,1000p" | awk '{print $1,$9,$11}' | showProcessAllthreads
rm -rf /tmp/${uniqueId}