崩溃或致命错误导致进程异常终止。有各种可能的理由导致崩溃。例如,崩溃可能是由于HotSpot VM、系统库、Java SE 库或API、程序本地代码、甚至操作系统里的 bug。极端因素如操作系统资源耗尽也可以导致崩溃。
因 HotSpot VM 或 Java SE库代码导致的崩溃是罕见的。本章提供如何检查崩溃的建议。有时候可以变通崩溃直到导致崩溃的源被诊断和修复(也就是可以避开崩溃)。
通常,崩溃的第一步是定位致命错误日志。这是HotSpot VM生成的文本文件。附录C-致命错误日志 解释了如何定位文件和文件的详细描述。
4.1 崩溃样本
本节展示一些样本来说明错误日志是如何用于启发崩溃原因的。
4.1 测定哪里发生崩溃
错误日志头显示了有问题的帧。见 C.3 格式头。
如果顶层帧是本地帧且不是操作系统本地帧,这表明问题可能发生在本地库,而不是在JVM里。解决崩溃的第一步是研究本地库发生崩溃的源。有三个选择,取决于本地库的源。
如果本地库是由你的程序提供,研究你的本地库的源代码。选项 -Xcheck:jni
可以帮助查找本地 bug。见 B.2.1 -Xcheck:jni
选项。
如果你的程序使用的本地库是由其他供应商提供,报告bug,提供致命错误日志信息。
4.1.2 在本地代码里崩溃
如果致命错误日志表明崩溃是在本地库,那么可能有bug在本地代码或JNI库代码。崩溃当然也可以被其他因素导致,但分析库和任何core文件或崩溃转储是个好的开始点。例如,考虑下嘛的致命错误日志头:
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# SIGSEGV (0xb) at pc=0x417789d7, pid=21139, tid=1024
#
# Java VM: Java HotSpot(TM) Server VM (6-beta2-b63 mixed mode)
# Problematic frame:
# C [libApplication.so+0x9d7]
在这种情况下线程在 libApplication.so
里执行时发生 SIGSEGV
。
有时候本地库里的bug显示它自己是在JVM代码里崩溃。考虑下面的崩溃,线程 JavaThread
在 _thread_in_vm
状态(意味着它正在执行 JVM 代码)失败:
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x08083d77, pid=3700, tid=2896
#
# Java VM: Java HotSpot(TM) Client VM (1.5-internal mixed mode)
# Problematic frame:
# V [jvm.dll+0x83d77]
--------------- T H R E A D ---------------
Current thread (0x00036960): JavaThread "main" [_thread_in_vm, id=2896]
:
Stack: [0x00040000,0x00080000), sp=0x0007f9f8, free space=254k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x83d77]
C [App.dll+0x1047] <========= C/native frame
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub
V [jvm.dll+0x80f13]
V [jvm.dll+0xd3842]
V [jvm.dll+0x80de4]
V [jvm.dll+0x87cd2]
C [java.exe+0x14c0]
C [java.exe+0x64cd]
C [kernel32.dll+0x214c7]
:
在这种情况下,栈轨迹显示了 App.dll
里的本地例程调用到了VM里(可能用JNI)。
如果你在本地程序库遇到崩溃,如果可以,你可能能够关联本地调试器到core文件或崩溃转储。取决于操作系统,本地调试器是 dbx, gdb 或 windbg。
另一个方法是运行带 -Xcheck:jni
选项运行,这个选项不保证找出JNI代码里的所有问题,但它可以帮助识别问题issue的显著数字。
如果发生崩溃的本地库是JRE的一部分,那么可能是你遇到了一个库或API bug。
4.1.3 由于栈溢出崩溃
Java语言代码里的栈溢出通常将导致线程抛出 java.lang.StackOverflowError
。另一方面, C and C++ write past the end of the stack and provoke a stack overflow。这个致命错误将导致进程终止。
在HotSpot实现里,Java方法与C/C++本地代码共享栈帧,也就是用户本地代码和VM自身。Java方法生成代码来检查,距栈结束有个固定距离的可得栈空间,这样可以调用本地代码而不会超出栈空间。这个朝向栈结束的距离叫作 “阴影页,Shadow Pages”。阴影页的大小在 3-20
个(内存)页之间,取决于平台。这个距离是可调的,这样程序的本地代码需要多于默认距离的本地代码可以增加阴影页的大小。增加阴影页的选项是 -XX:StackShadowPages= n
,n
大于平台的默认栈阴影页。
如果程序碰到段错误且没有core文件、或致命错误日志文件、或 在Windows 上的 STACK_OVERFLOW_ERROR
、或 消息 “ An irrecoverable stack overflow has occurred ”,这表明 StackShadowPages
的值被超过了,需要更多空间。
如果你增加 StackShadowPages
的值,你可能也需要增加线程默认栈大小,用 -Xss
参数。增加线程默认栈大小可能需要减少被创建的线程,所以小心选择线程栈的大小。不同平台的栈大小从 256k - 1024k
不等。
下面是来自致命错误日志的一个段错误,在Windows系统上,线程在本地代码里激起一个栈溢出:
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# EXCEPTION_STACK_OVERFLOW (0xc00000fd) at pc=0x10001011, pid=296, tid=2940
#
# Java VM: Java HotSpot(TM) Client VM (1.6-internal mixed mode, sharing)
# Problematic frame:
# C [App.dll+0x1011]
#
--------------- T H R E A D ---------------
Current thread (0x000367c0): JavaThread "main" [_thread_in_native, id=2940]
:
Stack: [0x00040000,0x00080000), sp=0x00041000, free space=4k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [App.dll+0x1011]
C [App.dll+0x1020]
C [App.dll+0x1020]
:
C [App.dll+0x1020]
C [App.dll+0x1020]
...<more frames>...
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub
注意上面输出的下面信息:
- 异常是
EXCEPTION_STACK_OVERFLOW
。 - 线程的状态是
_thread_in_native
,这意味着线程在执行本地或JNI代码。 - 栈信息里的空闲空间只有 4k(Windows 系统上单个页面)。另外栈指针sp是 0x00041000 ,非常接近于栈结束 0x00040000。
- 本地帧输出显示,这种情况下问题是本地递归函数。
- 输出标记
...<more frames>..
表明了存在额外的帧,但没有输出。输出限制在 100 帧。
4.1.4 HotSpot 编译线程里崩溃
如果致命错误日志输出显示 Current thread
是命名为 CompilerThread0, CompilerThread1
或 AdapterCompiler
的 JavaThread
,这很可能是你碰到一个编译器bug。在这种情况下,你可能需要临时绕过问题,通过切换编译器或排除编译激起崩溃的方法。
4.1.5 在已编译代码里崩溃
如果崩溃发生在已编译代码里,可能是你遇到一个编译器bug,导致生成不正确的代码。你可以识别已编译代码里的崩溃,如果有问题的帧标记了编码 J
(意味着一个已编译Java帧)。下面是这类崩溃的示例:
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# SIGSEGV (0xb) at pc=0x0000002a99eb0c10, pid=6106, tid=278546
#
# Java VM: Java HotSpot(TM) 64-Bit Server VM (1.6.0-beta-b51 mixed mode)
# Problematic frame:
# J org.foobar.Scanner.body()V
#
:
Stack: [0x0000002aea560000,0x0000002aea660000), sp=0x0000002aea65ddf0,
free space=1015k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
J org.foobar.Scanner.body()V
[error occurred during error reporting, step 120, id 0xb]
注意完整的线程栈不可得。输出行 error occurred during error reporting
意味着在获取栈轨迹时出现错误(这个例子里可能是栈腐化corruption)。
可能需要临时绕过问题,通过切换编译器(如用HotSpot Client VM替代HotSpot Server VM,或相反)或排除编译激起崩溃的方法。在这个特定例子里,可能不能切换编译器从64-bit Server VM 切换到 32-bit Client VM。(Client VM 没有64位?)
4.1.6 VMThread 里崩溃
如果致命日志输出显示 Current thread
是 VMThread
,查找 THREAD
节里包含 VM_Operation
的行。VMThread 是HotSpot VM里的特殊线程。它在VM里执行特殊任务,如垃圾回收(GC)。VM_Operation
暗示是垃圾回收操作,那么你可能遇到了堆腐化的问题。
崩溃也可能是个GC问题,但它同样也可以是其他,使堆里的对象引用处于不一致或不正确状态。在这种情况下,收集尽可能多的环境信息,尝试可能的变通方案。如果问题是GC有关的,你可能可以临时绕过问题,通过改变GC配置。
4.2 找出变通方案
如果关键程序发生崩溃,且崩溃表现出是由于HotSpot VM里的bug导致,那么它值得快速找出一个临时变通方案。本节的目标是启发可能的变通方案。
注意:即使本节的变通方案成功消除了崩溃,变通方案也不是问题的修复,仅仅是临时解决方案。 Submit a support call or bug report with the original configuration that emonstrated the issue.
4.2.1 HotSpot 编译器线程或已编译代码里崩溃
如果致命错误日志表明崩溃发生在编译器线程,可能是你遇到了一个编译器bug。类似地,如果崩溃发生在已编译代码里,可能是编译器生成了不正确代码。
对于 HotSpot Client VM,编译器线程以 CompilerThread0
出现在错误日志里。 HotSpot Server VM 有多个编译器线程,在错误日志文件里表现为 CompilerThread0, CompilerThread1, AdapterThread
。
下面是遇到编译器bug的错误日志片段,bug在Java SE 5.0开发中修复。日志文件显示使用了 HotSpot Server VM,崩溃在 CompilerThread1
里发生。另外,日志文件显示了 Current CompileTask
是在编译 java.lang.Thread.setPriority
方法。
# An unexpected error has been detected by HotSpot Virtual Machine:
#
:
# Java VM: Java HotSpot(TM) Server VM (1.5-internal-debug mixed mode)
:
--------------- T H R E A D ---------------
Current thread (0x001e9350): JavaThread "CompilerThread1" daemon [_thread_in_vm, id=20]
Stack: [0xb2500000,0xb2580000), sp=0xb257e500, free space=505k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [libjvm.so+0xc3b13c]
:
Current CompileTask:
opto: 11 java.lang.Thread.setPriority(I)V (53 bytes)
--------------- P R O C E S S ---------------
Java Threads: ( => current thread )
0x00229930 JavaThread "Low Memory Detector" daemon [_thread_blocked, id=21]
=>0x001e9350 JavaThread "CompilerThread1" daemon [_thread_in_vm, id=20]
:
这种情况有两个潜在的变通方案:
- 暴力方法:修改配置,这样程序以
-client
选项来指定 HotSpot Client VM 来运行。 - 假设bug只发生在编译
setPriority
方法,那么从排除编译此方法。
第一种方法(使用 -client
选项)对有些环境可能是微不足道的。对于其它,可能是困难的,如果配置是复杂的或配置VM的命令行不可访问。通常,从 HotSpot Server VM 切换到 HotSpot Client VM 会降低程序的峰值性能。取决于环境,这可能是可接受的,直到问题被诊断和修复。
第二种方法要求在程序的工作目录下创建文件 .hotspot_compiler
,文件示例:
exclude java/lang/Thread setPriority
通常这个文件的格式是 exclude CLASS METHOD
,CLASS 是全路径类名,METHOD是方法的名字,构造器方法指定为 <init>
,静态初始化器指定为 <clinit>
。
注意:
.hotspot_compiler
文件不是受支持的。
一旦程序重新启动,编译器将不再尝试编译 .hotspot_compiler
文件里列出的任何方法。
为了核实HotSpot VM正确定位和处理了 .hotspot_compiler
文件,在运行时查找下面的日志信息。注意文件名是用句点分隔,而不是斜线。
### Excluding compile: java.lang.Thread::setPriority
4.2.2 垃圾回收时崩溃
如果崩溃在垃圾回收时发生,那么致命错误日志报告正在进行一个 VM_Operation
。为了讨论的目的,假设最主要的并发GC(-XX:+UseConcMarkSweep
)未使用。 VM_Operation
显示在日志的 THREAD
区域,表明下面的情境之一:
- Generation collection for allocation
- Full generation collection
- Parallel gc failed allocation
- Parallel gc failed permanent allocation
- Parallel gc system gc
日志里报告的当前线程最可能是 VMThread
。这是特殊线程用于执行 HotSpot VM 里的特定任务。下面的致命错误日志片段显示了串行垃圾收集器崩溃的一个举例:
--------------- T H R E A D ---------------
Current thread (0x002cb720): VMThread [id=3252]
siginfo: ExceptionCode=0xc0000005, reading address 0x00000000
Registers:
EAX=0x0000000a, EBX=0x00000001, ECX=0x00289530, EDX=0x00000000
ESP=0x02aefc2c, EBP=0x02aefc44, ESI=0x00289530, EDI=0x00289530
EIP=0x0806d17a, EFLAGS=0x00010246
Top of Stack: (sp=0x02aefc2c)
0x02aefc2c: 00289530 081641e8 00000001 0806e4b8
0x02aefc3c: 00000001 00000000 02aefc9c 0806e4c5
0x02aefc4c: 081641e8 081641c8 00000001 00289530
0x02aefc5c: 00000000 00000000 00000001 00000001
0x02aefc6c: 00000000 00000000 00000000 08072a9e
0x02aefc7c: 00000000 00000000 00000000 00035378
0x02aefc8c: 00035378 00280d88 00280d88 147fee00
0x02aefc9c: 02aefce8 0806e0f5 00000001 00289530
Instructions: (pc=0x0806d17a)
0x0806d16a: 15 08 83 3d c0 be 15 08 05 53 56 57 8b f1 75 0f
0x0806d17a: 0f be 05 00 00 00 00 83 c0 05 a3 c0 be 15 08 8b
Stack: [0x02ab0000,0x02af0000), sp=0x02aefc2c, free space=255k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x6d17a]
V [jvm.dll+0x6e4c5]
V [jvm.dll+0x6e0f5]
V [jvm.dll+0x71771]
V [jvm.dll+0xfd1d3]
V [jvm.dll+0x6cd99]
V [jvm.dll+0x504bf]
V [jvm.dll+0x6cf4b]
V [jvm.dll+0x1175d5]
V [jvm.dll+0x1170a0]
V [jvm.dll+0x11728f]
V [jvm.dll+0x116fd5]
C [MSVCRT.dll+0x27fb8]
C [kernel32.dll+0x1d33b]
VM_Operation (0x0373f71c): generation collection for allocation, mode:
safepoint, requested by thread 0x02db7108
注意:在垃圾回收时发生崩溃不一定表示垃圾收集实现有bug。它也可以表明编译器或运行时bug或其他问题。
如果你在垃圾收集时得到一个重复的崩溃,可以尝试下面的临时解决方案:
- 切换 GC 配置。例如,如果你使用串行收集器,尝试使用吞吐量优先收集器,或相反。
- 如果你使用 HotSpot Server VM,尝试 HotSpot Client VM。
如果你不确定在使用的垃圾收集器是哪个,可以用 jmap 实用程序从core 文件获取堆信息,如果core文件可得。通常,如果GC配置没有在命令行指定,在Windows上将使用串行收集器。在Solaris OS和Linux上,取决于机器配置。如果机器有至少2GB内存和至少2个处理器,将使用吞吐量收集器(并行GC)。对于小一点的机器,默认使用串行收集器。-XX:+UseSerialGC
指定使用串行收集器,-XX:+UseParallelGC
选项选择 吞吐量收集器。
4.2.3 类数据共享,Class data sharing
这个东西在HotSpot VM里限制较多,TODO。
4.3 Microsoft Visual C++ Version Considerations
TODO
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。