获取Java线程转储的常用方法

Posted 铁锚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了获取Java线程转储的常用方法相关的知识,希望对你有一定的参考价值。

文章目录

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 this help 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.2 Ctrl + Break (Windows)

在Windows操作系统的命令行窗口中, 可使用组合键 Ctrl + Break 来获取线程转储。 当然, 需要先导航至启动Java程序的控制台窗口, 然后同时按下 CTRL键和Break键。

需要注意的是, 某些键盘是没有 “Break” 键的。
在这种情况下, 可以组合使用 CTRL, SHIFT, 以及 Pause键。

这两个命令都可以将线程转储打印到控制台。

4. 通过编程方式使用ThreadMxBean

JMX技术支持各种各样的花式操作。 可通过 ThreadMxBean 来执行线程转储。

示例代码如下:

private static String threadDump(boolean lockedMonitors, boolean lockedSynchronizers) 
    StringBuffer threadDump = new StringBuffer(System.lineSeparator());
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    for(ThreadInfo threadInfo : threadMXBean.dumpAllThreads(lockedMonitors, lockedSynchronizers)) 
        threadDump.append(threadInfo.toString());
    
    return threadDump.toString();

上面代码做的事情很简单, 先通过 ManagementFactory 获取 ThreadMxBean 对象。
方法的布尔参数 lockedMonitorslockedSynchronizers, 表示是否导出持有的同步器和管程锁。

但是, 这种方法有一些缺陷:

    1. 性能不太好, 消耗的资源不少。
    1. threadDump.toString() 方法最多只会输出8个栈帧(MAX_FRAMES = 8); 可以拷贝 toString 代码并自己进行修改/过滤。
    1. 本地线程(比如GC线程)不会被Dump。

替代方案:

    1. 通过 Runtime 调用 jstack 获取线程转储信息; 如果失败则回退到JMX方式;

部分代码:


    public static String jStackThreadDump() 
        // 获取当前JVM进程的pid
        long currentPid = currentPid();
        // 组装命令
        String cmdarray[] = 
                "jstack",
                "" + currentPid
        ;
        ProcessBuilder builder = new ProcessBuilder(cmdarray);
        String threadDump = "";
        try 
            Process p = builder.start();
            final BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
            StringJoiner sj = new StringJoiner(System.lineSeparator());
            reader.lines().iterator().forEachRemaining(sj::add);
            threadDump = sj.toString();
            p.waitFor();
            p.destroy();
         catch (Throwable e) 
            e.printStackTrace();
        
        return threadDump;
    

    public static long currentPid() 
        final long fallback = -1;
        final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
        final int 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;

// 简单模拟线程的各种状态
public class ThreadStateTest implements Runnable 
    public final Lock lock = new ReentrantLock(true);
    public final CountDownLatch beforeMonitorLatch = new CountDownLatch(1);
    public final CountDownLatch beforeLockLatch = new CountDownLatch(1);
    public final CountDownLatch toSleepLatch = new CountDownLatch(1);

    public static void main(String[] args) throws Exception 
        // Runnable task
        ThreadStateTest task = new ThreadStateTest();
        // 新创建线程对象
        Thread thread = new Thread(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());
    

    @Override
    public void run() 
        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()===");
    

    // 工具方法; 程序断言相等
    static public void assertEquals(Object expected, Object actual) 
        if (false == Objects.equals(expected, actual)) 
            throw new RuntimeException("Not Equals: expected=" + expected + "; actual=" + actual);
        
    


控制台输出的执行结果为:

1. before start: thread.getState(): NEW
=== enter run() ===
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
===before sleep()===
5.Thread in sleep: thread.getState(): TIMED_WAITING
===finish run()===
6. after join: thread.getState(): TERMINATED

相关链接:

更多信息科参考:

以上是关于获取Java线程转储的常用方法的主要内容,如果未能解决你的问题,请参考以下文章

kill -3 获取java线程转储

Jvm常用参数

Tomcat 线程转储

11.5-全栈Java笔记:线程基本信息和优先级别

获取JVM堆内存转储的常用方法

线程中会有的问题