JVM 如何在底层收集 ThreadDump

srg*_*321 3 java jvm jvm-hotspot jvmti perf

请解释 JVM 如何在底层收集 ThreadDump。
我不明白它如何收集脱离 CPU 的线程的堆栈跟踪(等待磁盘 IO、网络、非自愿上下文切换)。
例如,linux perf 仅收集有关 CPU 线程(使用 CPU 周期)的信息

apa*_*gin 8

我将以 HotSpot JVM 为例。

JVM 维护所有 Java 线程的列表:对于每个线程,它都有一个相应的 VM 结构。根据其执行上下文,线程可以处于以下状态之一(HotSpot 知道每个线程的当前状态,因为它负责切换状态):

  • in_Java- 线程正在解释器中或 JIT 编译的方法中执行 Java 代码;
  • in_vm- 线程位于 VM 运行时函数内;
  • in_native- 线程正在 JNI 上下文中运行本机方法;
  • 还有过渡状态,但为了简单起见,让我们跳过它们。

一个off-cpu线程只能有

  • in_native状态:所有套接字 I/O、磁盘 I/O 和其他阻塞操作仅在本机代码中执行;
  • in_vm当线程在 VM 互斥体上被阻塞时的状态。

每当 JVM 调用本机方法或获取竞争互斥体时,它都会将最后一个 Java 帧指针存储到Thread结构中。

现在是关键部分: HotSpot JVM 仅在安全点获取线程转储

当您请求线程转储时,JVM 会请求暂停。所有线程在in_Java处于状态的线程都停止在最近的安全点,JVM 知道如何遍历堆栈。

处于状态的线程in_native不会停止,但它们不需要停止。HotSpot 知道它们的最后一个 Java 帧,因为指针存储在一个Thread结构中。知道了顶层 Java 框架,JVM 就可以找到它的调用者,然后找到调用者的调用者,依此类推。

栈行走

这里重要的是,无论本机方法做什么,堆栈的 Java 部分都是“冻结”的。堆栈的顶部部分(本机)可以来回更改,而底部部分(Java)则保持不可变。它无法更改,因为 JVM 在每次切换时都会检查挂起的安全点in_native操作in_Java切换时都会检查挂起的安全点操作:如果本机方法返回,并且 VM 当前正在运行停止世界操作,则当前线程将阻塞,直到操作结束。

因此,获取线程转储涉及

  1. 在安全点停止所有in_Java线程;in_vm
  2. 遍历 JVM 维护的全局线程列表;
  3. 如果一个线程正在运行native方法,它的顶级Java框架存储在一个线程结构中;如果线程正在运行 Java 代码,则其顶部框架对应于当前正在执行的 Java 方法。
  4. 每个帧都有一个到前一帧的链接,因此给定顶部帧,JVM 可以构造到底部的整个堆栈跟踪。