文章目录
1. 线程转储简介
线程转储(Thread Dump)就是JVM中所有线程状态信息的一次快照。
线程转储一般使用文本格式, 可以将其保存到文本文件中, 然后人工查看和分析, 或者使用工具/API自动分析。
Java中的线程模型, 直接使用了操作系统的线程调度模型, 只进行简单的封装。
线程调用栈, 也称为方法调用栈。 比如在程序执行过程中, 有一连串的方法调用链:obj1.method2
调用了obj2.methodB
,obj2.methodB
又调用了obj3.methodC
。 每个线程的状态都可以通过这种调用栈来表示。
线程转储展示了各个线程的行为, 对于诊断和排查问题非常有用。
下面我们通过具体示例, 来演示各种获取Java线程转储的工具, 以及使用方法。
2. 使用JDK自带的工具
我们一般使用JDK自带的命令行工具来获取Java应用程序的线程转储。 这些工具都在JDK主目录的bin文件夹下。
所以, 只要配置好 PATH 路径即可。 如果不会配置, 可以参考:JDK环境准备
2.1 jstack 工具
jstack 是JDK内置的一款命令行工具, 专门用来查看线程状态, 也可以用来执行线程转储。
一般先通过jps
或者ps
命令找到Java进程对应的pid, 然后在控制台中通过pid来输出线程转储。 当然, 我们也可以将输出内容重定向到某个文件中。
使用jstack工具获取线程转储的基本参数格式为:
jstack[-F][-l][-m]<pid>
下面请看具体的演示:
# 1. 查看帮助信息
jstack -help
输出的内容类似于:
Usage:
jstack[-l]<pid>(to connect to running process)
jstack -F[-m][-l]<pid>(to connect to a hung process)
jstack[-m][-l]<executable><core>(to connect to a core file)
jstack[-m][-l][server_id@]<remote server IP or hostname>(to connect to a remote debug server)
Options:
-F to force a thread dump. Use when jstack<pid> does not respond(process is hung)
-m to print both java and native frames(mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print thishelp message
对应的参数选项是可选的。 具体含义如下:
-F
选项, 强制执行线程转储; 有时候jstack pid
会假死, 则可以加上-F
标志-l
选项, 会查找堆内存中拥有的同步器以及资源锁-m
选项, 额外打印 native栈帧(C和C++的)
例如, 获取线程转储并将结果输出到文件:
jstack -F 17264> /tmp/threaddump.txt
使用jps
命令可以获取本地Java进程的 pid。
2.2 Java Mission Control
Java Mission Control(JMC)是一款客户端图形界面工具, 用于收集和分析Java应用程序的各种数据。
启动JMC后, 首先会显示本地计算机上运行的Java进程列表。 当然也可以通过JMC连接到远程Java进程。
可以鼠标右键单击对应的进程, 选择 “Start Flight Recording(开始飞行记录)” 。 结束之后, “Threads(线程)” 选项卡会显示“线程转储”:
2.3 jvisualvm
jvisualvm 是一款客户端图形界面工具, 既简单又实用, 可用来监控 Java应用程序, 对JVM进行故障排查和性能分析。
也可以用来获取线程转储。 鼠标右键单击Java进程, 选择“ Thread Dump”选项, 则可以创建线程转储, 完成后会在新选项卡中自动打开:
2.4 jcmd
jcmd工具本质上是向目标JVM发送一串命令。 尽管支持很多功能, 但不支持连接远程JVM - 只能在Java进程的本地机器上使用。
其中一个命令是Thread.print
, 用来获取线程转储, 示例用法如下:
jcmd 17264 Thread.print
2.5 jconsole
jconsole 工具也可以查看线程栈跟踪。
打开jconsole并连接到正在运行的Java进程, 导航到“线程”选项卡, 可以查看每个线程的堆栈跟踪:
2.6 小结
事实证明, 可以使用JDK中的很多工具来获取线程转储。 让我们回顾一下, 并总结它们的优缺点:
jstack
:获取线程转储最简单最方便的工具; Java 8之后可以使用 jcmd 工具来替代;jmc
:增强的JDK性能分析和问题诊断工具。 用这款工具进行性能分析的开销非常低。jvisualvm
:轻量级的开源分析工具, 图形界面非常棒, 还支持各种强悍的功能插件。jcmd
: 非常强大的本地工具, 支持Java 8及更高版本。 集成了多种工具的作用, 例如: 捕获线程转储(jstack), 堆转储(jmap), 查看系统属性和查看命令行参数(jinfo)jconsole
:也可以用来查看线程栈跟踪信息。
3. 使用Linux命令
在企业应用服务器中, 出于安全原因, 可能只安装了 JRE。 这时候没法使用这些JDK内置的工具。
但还是有办法获取线程转储。
3.1 使用kill -3
指令
在Unix/Linux之类的系统中, 可以使用kill
命令获取线程转储, 底层实现原理, 则是通过系统调用kill()
将信号参数发送给进程。 这里需要发送的是-3
信号。
一般先通过jps
找到JAVA进程对应的pid,kill -3
使用示例如下:
kill -3 17264
3.2Ctrl + Break
(Windows)
在Windows操作系统的命令行窗口中, 可使用组合键Ctrl + Break
来获取线程转储。 当然, 需要先导航至启动Java程序的控制台窗口, 然后同时按下CTRL
键和Break
键。
需要注意的是, 某些键盘是没有 “Break
” 键的。
在这种情况下, 可以组合使用CTRL
,SHIFT
, 以及Pause
键。
这两个命令都可以将线程转储打印到控制台。
4. 通过编程方式使用ThreadMxBean
JMX技术支持各种各样的花式操作。 可通过ThreadMxBean
来执行线程转储。
示例代码如下:
privatestatic StringthreadDump(boolean lockedMonitors,boolean lockedSynchronizers){
StringBuffer threadDump=newStringBuffer(System.lineSeparator());
ThreadMXBean threadMXBean= ManagementFactory.getThreadMXBean();for(ThreadInfo threadInfo: threadMXBean.dumpAllThreads(lockedMonitors, lockedSynchronizers)){
threadDump.append(threadInfo.toString());}return threadDump.toString();}
上面代码做的事情很简单, 先通过ManagementFactory
获取ThreadMxBean
对象。
方法的布尔参数lockedMonitors
和lockedSynchronizers
, 表示是否导出持有的同步器和管程锁。
但是, 这种方法有一些缺陷:
- 性能不太好, 消耗的资源不少。
threadDump.toString()
方法最多只会输出8个栈帧(MAX_FRAMES = 8
); 可以拷贝 toString 代码并自己进行修改/过滤。
- 本地线程(比如GC线程)不会被Dump。
替代方案:
- 通过 Runtime 调用 jstack 获取线程转储信息; 如果失败则回退到JMX方式;
部分代码:
publicstatic StringjStackThreadDump(){// 获取当前JVM进程的pidlong currentPid=currentPid();// 组装命令
String cmdarray[]={"jstack",""+ currentPid};
ProcessBuilder builder=newProcessBuilder(cmdarray);
String threadDump="";try{
Process p= builder.start();final BufferedReader reader=newBufferedReader(newInputStreamReader(p.getInputStream()));
StringJoiner sj=newStringJoiner(System.lineSeparator());
reader.lines().iterator().forEachRemaining(sj::add);
threadDump= sj.toString();
p.waitFor();
p.destroy();}catch(Throwable e){
e.printStackTrace();}return threadDump;}publicstaticlongcurrentPid(){finallong fallback=-1;final String jvmName= ManagementFactory.getRuntimeMXBean().getName();finalint index= jvmName.indexOf("@");if(index<1){return fallback;}
String pid= jvmName.substring(0, index);if(null!= pid&& pid.matches("\\d+")){return Long.parseLong(pid);}return fallback;}
5. 总结
我们通过具体示例展示了获取线程转储的各种方法。
首先介绍的是各种JDK内置工具,
然后讨论了命令行方式,
最后介绍了JMX编程的方式。
完整的示例代码请参考GitHub仓库 。
6. 附录: 线程状态及示例代码
Thread 状态可参考Thread.State
, 包括:
NEW
: 未启动; 比如还没执行(完) start 方法;RUNNABLE
: 可运行状态; 这是JVM的视角, 具体是否正在使用CPU则看操作系统调度;BLOCKED
: 阻塞状态; 比如进入同步方法/同步块, 等待锁资源;WAITING
: 等待锁资源, 比如Unsafe.park()
,Object.wait()
等。TIMED_WAITING
: 限时等待锁资源, 比如Unsafe.park()
,Object.wait()
等。TERMINATED
: 已终结; 线程的任务已执行完了。
测试代码:
import java.util.Objects;import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;// 简单模拟线程的各种状态publicclassThreadStateTestimplementsRunnable{publicfinal Lock lock=newReentrantLock(true);publicfinal CountDownLatch beforeMonitorLatch=newCountDownLatch(1);publicfinal CountDownLatch beforeLockLatch=newCountDownLatch(1);publicfinal CountDownLatch toSleepLatch=newCountDownLatch(1);publicstaticvoidmain(String[] args)throws Exception{// Runnable task
ThreadStateTest task=newThreadStateTest();// 新创建线程对象
Thread thread=newThread(task);// 1. 线程未开始; NEW 状态;
System.out.println("1. before start: thread.getState(): "+ thread.getState());assertEquals(Thread.State.NEW, thread.getState());// 把重量锁抢了synchronized(task){// 启动线程;
thread.start();// 等待执行到要请求管程锁
task.beforeMonitorLatch.await();
TimeUnit.MILLISECONDS.sleep(100L);// 3. 线程在阻塞状态: 等待管程锁
System.out.println("3. blocked by monitor: thread.getState(): "+ thread.getState());assertEquals(Thread.State.BLOCKED, thread.getState());// 将轻量锁抢了
task.lock.lock();}// 等待执行到要请求锁
task.beforeLockLatch.await();// 稍微等一等
TimeUnit.MILLISECONDS.sleep(100L);// 4. 等待状态; 此处是等待轻量锁;
System.out.println("4. waiting lock: thread.getState(): "+ thread.getState());assertEquals(Thread.State.WAITING, thread.getState());// 释放锁
task.lock.unlock();// 让线程继续执行
task.toSleepLatch.countDown();
TimeUnit.MILLISECONDS.sleep(100);// 此时 thread 应该在睡眠中
System.out.println("5.Thread in sleep: thread.getState(): "+ thread.getState());assertEquals(Thread.State.TIMED_WAITING, thread.getState());// 等线程结束来汇合
thread.join();
System.out.println("6. after join: thread.getState(): "+ thread.getState());assertEquals(Thread.State.TERMINATED, thread.getState());}@Overridepublicvoidrun(){
System.out.println("=== enter run() ===");// 获取执行此任务的线程;
Thread thread= Thread.currentThread();// 2. 线程在执行过程中; 在JVM看来属于可执行状态assertEquals(Thread.State.RUNNABLE, thread.getState());
System.out.println("2. executing run: thread.getState(): "+ thread.getState());//请求管程锁
System.out.println("=== before synchronized (this)===");
beforeMonitorLatch.countDown();synchronized(this){
System.out.println("===synchronized (this) enter===");}// 设置标识: 即将请求轻量锁
beforeLockLatch.countDown();
System.out.println("===before lock.lock()===");// 等待锁
lock.lock();
lock.unlock();try{// 等待标志: 需要睡眠this.toSleepLatch.await();// 睡眠500毫秒
System.out.println("===before sleep()===");
TimeUnit.MILLISECONDS.sleep(500L);}catch(InterruptedException e){
e.printStackTrace();}
System.out.println("===finish run()===");}// 工具方法; 程序断言相等staticpublicvoidassertEquals(Object expected, Object actual){if(false== Objects.equals(expected, actual)){thrownewRuntimeException("Not Equals: expected="+ expected+"; actual="+ actual);}}}
控制台输出的执行结果为:
1. before start: thread.getState(): NEW=== enterrun()===2. executing run: thread.getState(): RUNNABLE=== before synchronized(this)===3. blocked by monitor: thread.getState(): BLOCKED===synchronized(this) enter======before lock.lock()===4. waiting lock: thread.getState(): WAITING===beforesleep()===5.Thread in sleep: thread.getState(): TIMED_WAITING===finishrun()===6. after join: thread.getState(): TERMINATED
相关链接:
更多信息科参考:
- 原文链接:https://www.baeldung.com/java-thread-dump
- JVMTI
- DTrace
- honest-profiler
- 中英双语对照版GitHub:获取Java线程转储的常用方法
- 中英双语对照版Gitee:获取Java线程转储的常用方法