本章为挂起或循环进程的故障排除在特定程序上提供了信息和指导。
问题在涉及挂起或循环进程时发生。挂起可能因为多种原因发生,但经常是源于程序代码、API代码或库代码里的死锁。挂起甚至是因为 HotSpot VM的bug。
有时候,一个表面上是挂起的可能是个循环。例如,VM进程里的bug导致一个或多个线程进入死循环,会消耗掉所有可得CPU周期。
诊断挂起的最初步骤是找出VM进程是空闲还是消耗了所有可得CPU周期,为做这个要求使用操作系统工具。如果进程表现为繁忙且消耗了所有可得CPU周期,那么问题很可能是循环线程而不是死锁。
5.1 诊断循环进程
如果VM进程表现为循环,第一步是尝试获取线程转储。如果获取了线程转储,通常哪个线程在循环是很明显的。如果循环线程被认定,线程转储里的栈轨迹可以提供线程在哪里(可能还有为什么)循环的方向。
如果程序控制台(标准输入输出)可得,按下 Ctrl-\
键组合(Solaris OS 或 Linux上)或 Ctrl-Break
键组合(Windows上)引起 HotSpot VM 打印线程转储,包括线程状态。在Solaris OS 和Linux上,线程转储还可以通过发送 SIGQUIT
到进程(命令:kill -QUIT <pid>
)来获得。在这种情况下,线程转储将被打印到目标进程的标准输出。输出也可以重定向到文件,取决于进程如果启动。
如果Java进程是带 -XX:+PrintClassHistogram
选项启动的, Ctrl-Break
处理将生成堆直方图histogram。
如果线程转储被获取了,runnable状态的线程的线程轨迹是个好的起点。线程转储的格式的更多信息见 2.15.1 节,还有线程转储里可能的线程状态表格。在有些情况下,可能需要获取一序列线程转储来确定哪个线程是持续繁忙的。
如果程序控制台不可得(进程运行在后台或VM输出被重定向到未知地方),jstack 工具可用于获取线程轨迹。用 jstack -F pid
选项来强制循环进程生成栈转储。从2.11节查看这个工具的输出的信息。jstack 工具也用于如果线程转储不能提供Java 线程循环的证据的情况。
当查看 jstack 工具的输出时,开始时专注于处于 RUNNABLE
状态的线程。这个状态很可能是因为线程是繁忙和循环。可能需要执行 jstack 多次来获得一个线程循环的完整图(译注:也就是通过对比一序列转储来确定)。如果一个线程总是表现为 RUNNABLE
状态,-m
选项可用于打印本地帧和提供线程在做什么的更多提示。如果线程在 RUNNABLE
状态表现出持续循环,这个情况指示了一个潜在的 HotSpot VM bug,需要进一步调查。
如果VM不响应 Ctrl-\
,这可能指示了VM bug而不是程序或库代码的问题issue。在这种情况下,jstack -m -F
可用于获取所有线程的栈。这个输出将包括VM内部线程的栈。在这个栈轨迹里,标识那些没有表现出等待的线程。
5.2 诊断挂起进程
如果程序表现出挂起且进程表现出空闲,那么第一步是尝试获取线程转储。如果程序控制台可得,按下 Ctrl-\
(Solaris OS 或 Linux上)或 Ctrl-Break
(Windows上)引起 HotSpot VM 打印线程转储 。在Solaris OS 和Linx上,线程转储还可以通过发送 SIGQUIT
到进程(命令:kill -QUIT <pid>
)来获得。
5.2.1 检测到死锁
如果挂起进程能够生成线程转储,输出将被打印到目标进程的标准输出。打印线程转储之后,HotSpot VM 执行一个死锁检测算法。如果检测到死锁,死锁将与涉及死锁的线程的栈轨迹一起输出。下面是输出示例:
Found one Java-level deadlock:
=============================
"AWT-EventQueue-0":
waiting to lock monitor 0x000ffbf8 (object 0xf0c30560, a java.awt.Component$AWTTreeLock),
which is held by "main"
"main":
waiting to lock monitor 0x000ffe38 (object 0xf0c41ec8, a java.util.Vector),
which is held by "AWT-EventQueue-0"
Java stack information for the threads listed above:
===================================================
"AWT-EventQueue-0":
at java.awt.Container.removeNotify(Container.java:2503)
- waiting to lock <0xf0c30560> (a java.awt.Component$AWTTreeLock)
at java.awt.Window$1DisposeAction.run(Window.java:604)
at java.awt.Window.doDispose(Window.java:617)
at java.awt.Dialog.doDispose(Dialog.java:625)
at java.awt.Window.dispose(Window.java:574)
at java.awt.Window.disposeImpl(Window.java:584)
at java.awt.Window$1DisposeAction.run(Window.java:598)
- locked <0xf0c41ec8> (a java.util.Vector)
at java.awt.Window.doDispose(Window.java:617)
at java.awt.Window.dispose(Window.java:574)
at javax.swing.SwingUtilities$SharedOwnerFrame.dispose(SwingUtilities.java:1743)
at javax.swing.SwingUtilities$SharedOwnerFrame.windowClosed(SwingUtilities.java:1722)
at java.awt.Window.processWindowEvent(Window.java:1173)
at javax.swing.JDialog.processWindowEvent(JDialog.java:407)
at java.awt.Window.processEvent(Window.java:1128)
at java.awt.Component.dispatchEventImpl(Component.java:3922)
at java.awt.Container.dispatchEventImpl(Container.java:2009)
at java.awt.Window.dispatchEventImpl(Window.java:1746)
at java.awt.Component.dispatchEvent(Component.java:3770)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:463)
at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:214)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)
"main":
at java.awt.Window.getOwnedWindows(Window.java:844)
- waiting to lock <0xf0c41ec8> (a java.util.Vector)
at javax.swing.SwingUtilities$SharedOwnerFrame.installListeners(SwingUtilities.java:1697)
at javax.swing.SwingUtilities$SharedOwnerFrame.addNotify(SwingUtilities.java:1690)
at java.awt.Dialog.addNotify(Dialog.java:370)
- locked <0xf0c30560> (a java.awt.Component$AWTTreeLock)
at java.awt.Dialog.conditionalShow(Dialog.java:441)
- locked <0xf0c30560> (a java.awt.Component$AWTTreeLock)
at java.awt.Dialog.show(Dialog.java:499)
at java.awt.Component.show(Component.java:1287)
at java.awt.Component.setVisible(Component.java:1242)
at test01.main(test01.java:10)
Found 1 deadlock.
默认的死锁检测可以和通过 synchronized
关键字获取的锁,还有通过 java.util.concurrent
包获取的锁一起工作。如果设置了 JVM 的 -XX:+PrintConcurrentLocks
标记,那么栈轨迹也显示锁属主的列表。
如果检测到死锁,你必须更详细地检验来理解死锁。在上面的例子里,线程 main
锁定了对象 <0xf0c30560>
,并等待进入 0xf0c41ec8
,它是由线程 AWT-EventQueue-0
锁定的。然而,线程 AWT-EventQueue-0
正在等待 0xf0c30560
,而它是由线程 main
锁定。
栈轨迹里的详细信息提供了查找死锁的帮助。
5.2.2 未检测到死锁
如果线程转储被打印且没有发现死锁,那么问题可能是个bug,线程在监视器上等待但从未收到通知。这可能是个定时timing问题或一般的逻辑错误。
为找出问题的更多信息,检查线程转储里的每个线程和每个阻塞在 Object.wait()
的线程。栈轨迹里的调用帧指示了正在调用 wait()
的类和方法。如果代码编译时(默认)包含了行号信息,这直接提供了检查的方向(译注:可以直接看到哪行源码在调用 wait 方法)。大多数情况下,为进一步诊断问题,你必须有程序逻辑或库的知识。通常,你必须明白程序的同步是如何工作的,特别是监视器何时、何地被通知的细节和条件。
5.2.3 没有线程转储
如果VM不响应 Ctrl-\
或 Ctrl-Break
,可能是VM死锁了或由于其它原因挂起了。在那样的情况下用 jstack 工具来获取线程转储。jstack -F pid
选项来强制挂起进程的线程转储。这也可以用于于程序不能访问或输出被重定向到未知地方。
在 jstack 的输出里,检查每个处于 BLOCKED
状态的线程。顶层帧有时能指示线程为什么被阻塞了,例如 Object.wait
或 Thread.sleep
。栈的其余部分将给出线程正在做什么。特别是源码被编译了行号信息,你可以交叉引用源代码。
如果线程处于 BLOCKED
状态且理由不明显,用 -m
选项来获取一个混合栈。在混合栈的输出里,你应该可以确定线程为什么被阻塞了。如果线程因为尝试进入同步方法或块而阻塞,你将在顶层帧附近看到类似 ObjectMonitor::enter
的帧。举例:
----------------- t@13 -----------------
0xff31e8b8 ___lwp_cond_wait + 0x4
0xfea8c810 void ObjectMonitor::EnterI(Thread*) + 0x2b8
0xfeac86b8 void ObjectMonitor::enter2(Thread*) + 0x250
:
处于 RUNNABLE
状态的线程也可能阻塞。混合栈里的顶层帧应当指示线程正在做什么。
一个需要检查的特定线程是 VMThread
。这个特定线程用于执行像垃圾回收的动作。它可以通过线程的初始栈是否正在执行 VMThread::run()
来鉴别。在Solaris OS上典型的是 t@4
,在Linux上是用 C++ mangled名字 _ZN8VMThread4loopEv
。
通常,你可以从命令行执行程序,你可以得到VM不响应 Ctrl-\
或 Ctrl-Break
的状态,很可能是你揭露了一个VM bug,一个线程库问题issue,或其他库的bug。如果这个发生,获取一个崩溃转储,尽可能多地收集信息,提交bug报告或支持电话。
在挂起进程上下文要提及的另一个工具是Solaris OS 的 pstack 工具。Linux上与 pstack
等价的工具是 lsstack
。在写这篇文档时 lsstack
只能报告本地栈帧。
TODO。
5.3 Solaris 8 OS 线程库
TODO
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。