Java SE 6 故障排除指南 – 5、挂起或循环进程故障排除

本章为挂起或循环进程的故障排除在特定程序上提供了信息和指导。

问题在涉及挂起或循环进程时发生。挂起可能因为多种原因发生,但经常是源于程序代码、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.waitThread.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笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据