现在我们来深入探讨线程转储及其分析方法。
本文还将解释线程转储如何帮助定位问题,并介绍一些可用的分析工具。
何谓线程?
进程是指加载到计算机内存中并正在执行的计算机程序。它可以由一个或多个处理器执行。进程在内存中通过存储诸如变量、文件句柄、程序计数器、寄存器和信号等关键信息进行描述。
一个进程可以细分为多个轻量级进程,称为线程。这种划分有助于实现并行处理,即将一个进程分解成多个线程,从而提升性能。进程内的所有线程共享相同的内存空间,且彼此依赖。
线程转储的作用
当进程运行时,我们可以使用线程转储来检查进程中各个线程的当前执行状态。线程转储捕捉程序执行过程中特定时间点所有活动线程的快照,并包含关于线程及其当前状态的所有相关信息。
当今的现代应用程序通常涉及多个线程。每个线程需要一定的资源来执行与进程相关的特定活动。这能够提升应用程序的性能,因为线程可以有效地利用可用的 CPU 核心。
然而,这种方式也存在一些权衡,例如,多个线程有时可能无法很好地协同工作,并可能出现死锁情况。因此,如果出现问题,我们可以通过分析线程转储来检查线程的状态。
Java 中的线程转储
JVM 线程转储是在特定时间点,作为进程组成部分的所有线程状态的列表。它包含了以堆栈跟踪形式呈现的线程堆栈信息。由于内容以纯文本形式记录,我们可以将其保存下来,方便日后查阅。线程转储的分析有助于:
- 优化 JVM 性能
- 提升应用程序性能
- 诊断诸如死锁、线程争用等问题
如何生成线程转储
有很多方法可以生成线程转储。以下是一些基于 JVM 的工具,可以从命令行/终端(CLI 工具)或 Java 安装文件夹的 /bin 目录(GUI 工具)执行。
让我们来详细了解一下。
#1. jstack
最简单的方法是使用 jstack 来生成线程转储。jstack 随 JVM 一起提供,并且可以从命令行使用。我们需要指定要生成线程转储的进程的 PID。要获取 PID,我们可以使用 jps 命令,如下所示。
jps -l
jps 命令列出所有 Java 进程 ID。
在 Windows 上
C:Program FilesJavajdk1.8.0_171bin>jps -l 47172 portal 6120 sun.tools.jps.Jps C:Program FilesJavajdk1.8.0_171bin>
在 Linux 上
[[email protected] ~]# jps -l 1088 /opt/keycloak/jboss-modules.jar 26680 /var/lib/jenkins/workspace/kyc/kyc/target/kyc-1.0.jar 7193 jdk.jcmd/sun.tools.jps.Jps 2058 /usr/share/jenkins/jenkins.war 11933 /var/lib/jenkins/workspace/admin-portal/target/portal-1.0.jar [[email protected] ~]#
如上所示,我们得到了所有正在运行的 Java 进程的列表。列表的第一列是正在运行的 Java 进程的本地 VM ID,第二列是应用程序的名称。现在,为了生成线程转储,我们使用带有 `-l` 标志的 jstack 程序,它会创建一个详细的转储输出。我们还可以将输出通过管道传输到指定的文本文件中。
jstack -l 26680
[[email protected] ~]# jstack -l 26680 2020-06-27 06:04:53 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode): "Attach Listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE Locked ownable synchronizers: - None "logback-8" #2316 daemon prio=5 os_prio=0 tid=0x00007f07e0033000 nid=0x4792 waiting on condition [0x00007f07baff8000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006ca9a1fc0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None "logback-7" #2315 daemon prio=5 os_prio=0 tid=0x00007f07e0251800 nid=0x4791 waiting on condition [0x00007f07bb0f9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006ca9a1fc0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None
#2. jvisualvm
Jvisualvm 是一款 GUI 工具,可帮助我们对 Java 应用程序进行故障排除、监控和分析。它也随 JVM 一起提供,可以从 Java 安装目录的 `/bin` 文件夹中启动。它非常直观且易于使用。在众多功能中,它还允许我们捕获特定进程的线程转储。
要查看特定进程的线程转储,我们可以右键单击该程序并从上下文菜单中选择“线程转储”。
#3. jcmd
jcmd 是 JDK 自带的命令行实用程序,用于向 JVM 发送诊断命令请求。
但是,它仅适用于运行 Java 应用程序的本地计算机。它可用于控制 Java Flight Recorder、诊断和排除 JVM 以及 Java 应用程序的故障。我们可以使用 jcmd 的 `Thread.print` 命令来获取由 PID 指定的特定进程的线程转储列表。
以下是如何使用 jcmd 的示例。
jcmd 28036 Thread.print
C:Program FilesJavajdk1.8.0_171bin>jcmd 28036 Thread.print 28036: 2020-06-27 21:20:02 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode): "Bundle File Closer" #14 daemon prio=5 os_prio=0 tid=0x0000000021d1c000 nid=0x1d4c in Object.wait() [0x00000000244ef000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Unknown Source) at org.eclipse.osgi.framework.eventmgr.EventManager$EventThread.getNextEvent(EventManager.java:403) - locked <0x000000076f380a88> (a org.eclipse.osgi.framework.eventmgr.EventManager$EventThread) at org.eclipse.osgi.framework.eventmgr.EventManager$EventThread.run(EventManager.java:339) "Active Thread: Equinox Container: 0b6cc851-96cd-46de-a92b-253c7f7671b9" #12 prio=5 os_prio=0 tid=0x0000000022e61800 nid=0xbff4 waiting on condition [0x00000000243ee000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076f388188> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(Unknown Source) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(Unknown Source) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.getTask(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source) "Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000021a7b000 nid=0x2184 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread3" #9 daemon prio=9 os_prio=2 tid=0x00000000219f5000 nid=0x1300 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x00000000219e0000 nid=0x48f4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x00000000219df000 nid=0xb314 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x00000000219db800 nid=0x2260 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000219d9000 nid=0x125c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000219d8000 nid=0x834 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001faf3000 nid=0x36c0 in Object.wait() [0x0000000021eae000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076f390180> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) - locked <0x000000076f390180> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000005806000 nid=0x13c0 in Object.wait() [0x00000000219af000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076f398178> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Unknown Source) at java.lang.ref.Reference.tryHandlePending(Unknown Source) - locked <0x000000076f398178> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source) "main" #1 prio=5 os_prio=0 tid=0x000000000570e800 nid=0xbf8 runnable [0x0000000000fec000] java.lang.Thread.State: RUNNABLE at java.util.zip.ZipFile.open(Native Method) at java.util.zip.ZipFile.<init>(Unknown Source) at java.util.zip.ZipFile.<init>(Unknown Source) at java.util.zip.ZipFile.<init>(Unknown Source) at org.eclipse.osgi.framework.util.SecureAction.getZipFile(SecureAction.java:307) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.getZipFile(ZipBundleFile.java:136) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.lockOpen(ZipBundleFile.java:83) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.getEntry(ZipBundleFile.java:290) at org.eclipse.equinox.weaving.hooks.WeavingBundleFile.getEntry(WeavingBundleFile.java:65) at org.eclipse.osgi.storage.bundlefile.BundleFileWrapper.getEntry(BundleFileWrapper.java:55) at org.eclipse.osgi.storage.BundleInfo$Generation.getRawHeaders(BundleInfo.java:130) - locked <0x000000076f85e348> (a java.lang.Object) at org.eclipse.osgi.storage.BundleInfo$CachedManifest.get(BundleInfo.java:599) at org.eclipse.osgi.storage.BundleInfo$CachedManifest.get(BundleInfo.java:1) at org.eclipse.equinox.weaving.hooks.SupplementerRegistry.addSupplementer(SupplementerRegistry.java:172) at org.eclipse.equinox.weaving.hooks.WeavingHook.initialize(WeavingHook.java:138) at org.eclipse.equinox.weaving.hooks.WeavingHook.start(WeavingHook.java:208) at org.eclipse.osgi.storage.FrameworkExtensionInstaller.startActivator(FrameworkExtensionInstaller.java:261) at org.eclipse.osgi.storage.FrameworkExtensionInstaller.startExtensionActivators(FrameworkExtensionInstaller.java:198) at org.eclipse.osgi.internal.framework.SystemBundleActivator.start(SystemBundleActivator.java:112) at org.eclipse.osgi.internal.framework.BundleContextImpl$3.run(BundleContextImpl.java:815) at org.eclipse.osgi.internal.framework.BundleContextImpl$3.run(BundleContextImpl.java:1) at java.security.AccessController.doPrivileged(Native Method) at org.eclipse.osgi.internal.framework.BundleContextImpl.startActivator(BundleContextImpl.java:808) at org.eclipse.osgi.internal.framework.BundleContextImpl.start(BundleContextImpl.java:765) at org.eclipse.osgi.internal.framework.EquinoxBundle.startWorker0(EquinoxBundle.java:1005) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle$EquinoxSystemModule.initWorker(EquinoxBundle.java:190) at org.eclipse.osgi.container.SystemModule.init(SystemModule.java:99) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle.init(EquinoxBundle.java:272) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle.init(EquinoxBundle.java:257) at org.eclipse.osgi.launch.Equinox.init(Equinox.java:171) at org.eclipse.core.runtime.adaptor.EclipseStarter.startup(EclipseStarter.java:316) at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:251) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:661) at org.eclipse.equinox.launcher.Main.basicRun(Main.java:597) at org.eclipse.equinox.launcher.Main.run(Main.java:1476) "VM Thread" os_prio=2 tid=0x000000001fae8800 nid=0x32cc runnable "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000005727800 nid=0x3264 runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000005729000 nid=0xbdf4 runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000572a800 nid=0xae6c runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000572d000 nid=0x588 runnable "GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000000000572f000 nid=0xac0 runnable "GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000005730800 nid=0x380 runnable "GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000005733800 nid=0x216c runnable "GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000005734800 nid=0xb930 runnable "VM Periodic Task Thread" os_prio=2 tid=0x0000000021a8d000 nid=0x2dcc waiting on condition JNI global references: 14 C:Program FilesJavajdk1.8.0_171bin>
#4. JMC
JMC 代表 Java 任务控制。它是一个随 JDK 一起提供的开源 GUI 工具,用于收集和分析 Java 应用程序数据。
它可以从我们的 Java 安装的 `/bin` 文件夹中启动。Java 管理员和开发人员使用该工具来收集有关 JVM 和应用程序行为的详细低级信息。它可以对 Java Flight Recorder 收集的数据进行详细、高效的分析。
启动 jmc 时,我们可以看到在本地计算机上运行的 Java 进程列表。远程连接也是可以的。在特定进程上,我们可以右键单击并选择“开始飞行记录”,然后在“线程”选项卡中检查线程转储。
#5. 控制台
jconsole 是一个用于投诉管理和监控的 Java 管理扩展工具。
它还具有一组用户可以在 JMX 代理上执行的预定义操作。它使用户能够检测和分析实时程序的堆栈跟踪。它可以从我们的 Java 安装的 `/bin` 文件夹中启动。
使用 jconsole GUI 工具,我们可以在将线程连接到正在运行的 Java 进程时检查每个线程的堆栈跟踪。然后,在“线程”选项卡中,我们可以看到所有正在运行的线程的名称。要检测死锁,我们可以单击窗口右下角的“检测死锁”。如果检测到死锁,它将显示在新选项卡中,否则将显示“未检测到死锁”。
#6. ThreadMxBean
ThreadMXBean 是 Java 虚拟机线程系统的管理接口,属于 `java.lang.Management` 包。它主要用于检测进入死锁情况的线程并获取关于它们的详细信息。
我们可以使用 ThreadMxBean 接口以编程方式捕获线程转储。`ManagementFactory` 的 `getThreadMXBean()` 方法用于获取 `ThreadMXBean` 接口的实例。它返回守护进程和非守护进程活动线程的数量。`ManagementFactory` 是一个工厂类,用于获取 Java 平台的托管 bean。
private static String getThreadDump (boolean lockMonitors, boolean lockSynchronizers) { StringBuffer threadDump = new StringBuffer (System.lineSeparator ()); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean (); for (ThreadInfo threadInfo : threadMXBean.dumpAllThreads (lockMonitors, lockSynchronizers)) { threadDump.append (threadInfo.toString ()); } return threadDump.toString (); }
手动分析线程转储
线程转储分析对于识别多线程进程中的问题非常有用。通过可视化各个线程转储的状态,可以解决诸如死锁、锁争用和个别线程的 CPU 使用率过高等问题。
通过在分析线程转储后纠正每个线程的状态,可以实现应用程序的最大吞吐量。
例如,假设一个进程正在使用大量 CPU,我们可以找出是否有线程消耗了最多的 CPU。如果有这样的线程,我们将其 LWP 编号转换为十六进制数。然后从线程转储中,我们可以找到 nid 等于之前得到的 16 进制数的线程。使用线程的堆栈跟踪,我们可以查明问题所在。让我们使用以下命令找出线程的进程 ID。
ps -mo pid,lwp,stime,time,cpu -C java
[[email protected] ~]# ps -mo pid,lwp,stime,time,cpu -C java PID LWP STIME TIME %CPU 26680 - Dec07 00:02:02 99.5 - 10039 Dec07 00:00:00 0.1 - 10040 Dec07 00:00:00 95.5
让我们看下面的线程转储块。要获取进程 26680 的线程转储,请使用 jstack -l 26680。
[[email protected] ~]# jstack -l 26680 2020-06-27 09:01:29 <strong>Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode):</strong> "Attach Listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE Locked ownable synchronizers: - None . . . . . . . "<strong>Reference Handler</strong>" #2 daemon prio=10 os_prio=0 tid=0x00007f085814a000 nid=0x6840 in Object.wait() [0x00007f083b2f1000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference.tryHandlePending(Reference.java:191) - locked <0x00000006c790fbd0> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) Locked ownable synchronizers: - None "VM Thread" os_prio=0 tid=0x00007f0858140800 nid=0x683f runnable "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f0858021000 nid=0x683b runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f0858022800 nid=0x683c runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f0858024800 nid=0x683d runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f0858026000 nid=0x683e runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f08581a0000 nid=0x6847 waiting on condition JNI global references: 1553
现在,让我们看看我们可以使用线程转储探索哪些内容。如果我们观察线程转储,我们可以看到很多内容,可能会让人感到无所适从。但是,如果我们一步一个脚印地进行,理解起来会相当简单。让我们了解第一行。
2020-06-27 09:01:29
全线程转储 Java HotSpot(TM) 64 位服务器 VM(25.221-b11 混合模式):
上面显示了生成转储的时间,以及关于使用的 JVM 的信息。接下来,最后,我们可以看到线程列表,其中第一个是我们的 `ReferenceHandler` 线程。
分析阻塞的线程
如果我们分析下面的线程转储日志,我们可以发现它检测到具有 `BLOCKED` 状态的线程,这会使应用程序的性能非常慢。因此,如果我们能够找到 `BLOCKED` 线程,我们可以尝试找出与线程试图获取的锁相关的线程。从当前持有锁的线程分析堆栈跟踪有助于解决问题。
[[email protected] ~]# jstack -l 26680 . . . . " DB-Processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000] java.lang.Thread.State: <strong>BLOCKED</strong> (on object monitor) at beans.ConnectionPool.getConnection(ConnectionPool.java:102) - waiting to lock <0xe0375410> (a beans.ConnectionPool) at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) "DB-Processor-14" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f020] java.lang.Thread.State: <strong>BLOCKED</strong> (on object monitor) at beans.ConnectionPool.getConnection(ConnectionPool.java:102) - waiting to lock <0xe0375410> (a beans.ConnectionPool) at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) . . . .
分析死锁线程
线程转储的另一个非常常用的应用是死锁检测。如果我们分析线程转储,死锁的检测和解决会容易得多。
死锁是指涉及至少两个线程的情况,其中一个线程继续执行所需的资源被另一个线程锁定,同时第二个线程所需的资源被第一个线程锁定。
因此,没有线程可以继续执行,这会导致死锁情况并导致应用程序卡住。如果存在