jvm常用命令工具之jstack
2023-05-23 17:12:12 阿炯

Jstack 是 java 虚拟机自带的一种堆栈跟踪工具,可以用来分析线程问题(如死锁)。

用于生成 java 虚拟机当前时刻的线程快照。线程快照是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。若 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外该工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息,如果现在运行的 java 程序呈现 hung 的状态,jstack 提供的信息是非常有用的。

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 [-l] pid’没有相应的时候强制打印栈信息 -l 长列表。打印关于锁的附加信息,例如属于 java.util.concurrent 的 ownable synchronizers 列表. -m 打印 java 和 native c/c++ 框架的所有栈信息. -h | -help 打印帮助信息 pid 需要被打印配置信息的 java 进程 id, 可以用 jps 查询.

线程状态
想要通过 jstack 命令来分析线程的情况的话,首先要知道线程都有哪些状态,下面这些状态是使用 jstack 命令查看线程堆栈信息时可能会看到的线程的几种状态:
NEW, 未启动的。不会出现在 Dump 中。
RUNNABLE, 在虚拟机内执行的。
BLOCKED, 受阻塞并等待监视器锁。
WATING, 无限期等待另一个线程执行特定操作。
TIMED_WATING, 有时限的等待另一个线程的特定操作。
TERMINATED, 已退出的。

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


进入区 (Entrt Set): 表示线程通过 synchronized 要求获取对象的锁。如果对象未被锁住,则迚入拥有者;否则则在进入区等待。一旦对象锁被其他线程释放,立即参与竞争。

拥有者 (The Owner): 表示某一线程成功竞争到对象锁。

等待区 (Wait Set): 表示线程通过对象的 wait 方法,释放对象的锁,并在等待区等待被唤醒。

从图中可以看出,一个 Monitor 在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set” 和 “Wait Set” 里面等候。在 “Entry Set” 中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set” 中等待的线程状态是 “in Object.wait ()”。 先看 “Entry Set” 里面的线程。称被 synchronized 保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set” 队列。

死锁分析
学会了怎么使用 jstack 命令之后,就可以看看如何使用 jstack 分析死锁了,这也是我们一定要掌握的内容。所谓死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

堆栈有明显提示,它告诉 Found one Java-level deadlock,然后指出造成死锁的两个线程的内容。然后又通过 Java stack information for the threads listed above 来显示更详细的死锁的信息。

调用修饰
表示线程在方法调用时,额外的重要的操作。线程 Dump 分析的重要信息。修饰上方的方法调用。

locked <地址> 目标:使用 synchronized 申请对象锁成功,监视器的拥有者。通过 synchronized 关键字,没有获取到了对象的锁,线程在监视器的进入区等待。在调用栈顶出现,线程状态为 Blocked。
waiting to lock <地址> 目标:使用 synchronized 申请对象锁未成功,在迚入区等待。通过 synchronized 关键字,成功获取到了对象的锁后,调用了 wait 方法,进入对象的等待区等待。在调用栈顶出现,线程状态为 WAITING 或 TIMED_WATING。
waiting on <地址> 目标:使用 synchronized 申请对象锁成功后,释放锁幵在等待区等待。
parking to wait for <地址> 目标。park 是基本的线程阻塞原语,不通过监视器在对象上阻塞。随 concurrent 包会出现的新的机制,不 synchronized 体系不同。

线程动作

线程状态产生的原因:
runnable: 状态一般为 RUNNABLE。
in Object.wait (): 等待区等待,状态为 WAITING 或 TIMED_WAITING。
waiting for monitor entry: 进入区等待,状态为 BLOCKED。
waiting on condition: 等待区等待、被 park。
sleeping: 休眠的线程,调用了 Thread.sleep ()。

Wait on condition 该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合 stacktrace 来分析。最常见的情况就是线程处于 sleep 状态,等待被唤醒。 常见的情况还有等待网络 IO:在 java 引入 nio 之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 NewIO 里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。正等待网络读写,这可能是一个网络瓶颈的征兆,因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat 统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制;观察 cpu 的利用率,如果系统态的 CPU 时间,相对于用户态的 CPU 时间比例较高;如果程序运行在 Solaris 10 平台上可以用 dtrace 工具看系统调用的情况,如果观察到 read/write 的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。

虚拟机执行 Full GC 时,会阻塞所有的用户线程。因此,即时获取到同步锁的线程也有可能被阻塞。在查看线程 Dump 时,首先查看内存使用情况。


参考来源

盘点一些java性能监控及工具