第 4 章
系统崩溃故障排除
本章提供有关解决系统崩溃问题的一些特定过程的信息和指导。
崩溃或致命错误会导致进程异常终止。崩溃有多种可能的原因。例如,由于 HotSpot VM、系统库、Java SE 库或 API、应用程序本机代码甚至操作系统中的错误,都可能发生崩溃。外部因素(例如操作系统中的资源耗尽)也可能导致崩溃。
HotSpot VM 或 Java SE 库代码中的错误导致的崩溃很少见。本章提供有关如何检查崩溃的建议。在某些情况下,在诊断并修复错误的原因之前,可能会解决崩溃问题。
一般来说,任何崩溃的第一步都是找到致命错误日志。这是 HotSpot VM 在发生崩溃时生成的文本文件。有关如何定位此文件的说明以及该文件的详细说明,请参见 附录 C,致命错误日志。
4.1 示例崩溃
本节提供了一些示例,展示了如何使用错误日志来提示崩溃的原因。
4.1.1 确定崩溃发生的位置
错误日志标头指示有问题的帧。请参阅 C.3 标题格式。
如果顶部帧类型是本机帧而不是操作系统本机帧之一,则这表明问题可能出在该本机库而不是 Java 虚拟机中。解决此崩溃的第一步是调查发生崩溃的本机库的来源。有三个选项,具体取决于本机库的来源。
- 如果您的应用程序提供了本机库,则调查您的本机库的源代码。该选项
-Xcheck:jni
可以帮助找到许多本机错误。见 B.2.1-Xcheck:jni
选项 。 - 如果本机库已由其他供应商提供并由您的应用程序使用,则针对此第三方应用程序提交错误报告并提供致命错误日志信息。
jre/lib
通过查看 JRE 分发中的或jre/bin
目录来确定本机库是否是 Java 运行时环境 (JRE) 的一部分 。如果是这样,请提交错误报告,并确保在显着位置标明此库名称,以便可以将错误报告发送给适当的开发人员。
如果错误日志中指示的顶部框架是另一种类型的框架,请提交错误报告并包含致命错误日志以及有关如何重现问题的任何信息。
另请参阅本章的其余部分。
4.1.2 原生代码崩溃
如果致命错误日志表明崩溃发生在本机库中,则本机代码或 JNI 库代码中可能存在错误。崩溃当然可能是由其他原因引起的,但是分析库和任何核心文件或崩溃转储是一个很好的起点。例如,考虑从致命错误日志的标题中提取的以下内容:
# 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]
在这种情况下, SIGSEGV
发生在库中执行的线程 libApplication.so
。
在某些情况下,本机库中的错误会表现为 Java VM 代码中的崩溃。考虑以下崩溃,其中 a JavaThread
在 _thread_in_vm
状态下失败(意味着它正在 Java VM 代码中执行):
# 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)。
如果您在本机应用程序库中遇到崩溃(如上面的示例),那么您可以将本机调试器附加到核心文件或故障转储(如果可用)。根据操作系统,本机调试器是 dbx
、 gdb
或 windbg
。
另一种方法是使用 -Xcheck:jni
添加到命令行的选项运行(请参阅 B.2.1 -Xcheck:jni
选项 )。此选项不能保证找到 JNI 代码的所有问题,但它可以帮助识别大量问题。
如果发生崩溃的本机库是 Java 运行时环境的一部分(例如 awt.dll
、 net.dll
等),那么您可能遇到了库或 API 错误。如果在进一步分析后您断定这是一个库或 API 错误,则收集尽可能多的数据并提交错误或支持电话。
4.1.3 堆栈溢出导致的崩溃
Java 语言代码中的堆栈溢出通常会导致有问题的线程抛出 java.lang.StackOverflowError
。另一方面,C 和 C++ 写入超出堆栈末尾并引发堆栈溢出。这是导致进程终止的致命错误。
在 HotSpot 实现中,Java 方法与 C/C++ 本机代码共享堆栈帧,即用户本机代码和虚拟机本身。Java 方法生成的代码检查堆栈空间在堆栈末端的固定距离是否可用,以便可以在不超出堆栈空间的情况下调用本机代码。靠近堆栈末端的这个距离称为“影子页”。影子页面的大小在 3 到 20 页之间,具体取决于平台。此距离是可调的,因此本机代码需要超过默认距离的应用程序可以增加影子页面大小。增加影子页面的选项是 -XX:StackShadowPages=
n,其中 n大于平台的默认堆栈影子页面。
如果您的应用程序在没有核心文件或致命错误日志文件(参见 附录 C,致命错误日志)或 STACK_OVERFLOW_ERROR
Windows 上出现分段错误或消息“发生不可恢复的堆栈溢出”,则表明 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
是 JavaThread
命名 CompilerThread0
的 CompilerThread1
、 或 AdapterCompiler
,那么您可能遇到了编译器错误。在这种情况下,可能需要通过切换编译器(例如,使用 HotSpot Client VM 而不是 HotSpot Server VM,或反之亦然)来临时解决此问题,或者从编译中排除引起崩溃的方法. 这在 4.2.1 HotSpot 编译器线程或编译代码中的崩溃中进行了讨论。
4.1.5 编译代码崩溃
如果崩溃发生在编译代码中,那么您可能遇到了导致错误代码生成的编译器错误。如果有问题的框架用代码标记 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
”表示在尝试获取堆栈跟踪时出现问题(在此示例中可能是堆栈损坏)。
可以通过切换编译器(例如,使用 HotSpot Client VM 而不是 HotSpot Server VM,或反之亦然)或通过从编译中排除引起崩溃的方法来临时解决此问题。在此特定示例中,可能无法切换编译器,因为它是从 64 位服务器 VM 获取的,因此切换到 32 位客户端 VM 可能不可行。
4.1.6 崩溃 VMThread
如果致命日志输出显示 Current thread
是 VMThread
,则查找 VM_Operation
该 THREAD
部分中包含的行。这 VMThread
是 HotSpot VM 中的一个特殊线程。它在 VM 中执行特殊任务,例如垃圾收集 (GC)。如果 VM_Operation
提示该操作是垃圾回收,那么您可能遇到了诸如堆损坏之类的问题。
崩溃也可能是 GC 问题,但也可能是其他原因(例如编译器或运行时错误)导致堆中的对象引用处于不一致或不正确的状态。在这种情况下,请尽可能多地收集有关环境的信息并尝试可能的解决方法。如果问题与 GC 相关,您可以通过更改 GC 配置来临时解决该问题。这在 4.2.2 垃圾收集期间的崩溃中进行了讨论。
4.2 寻找解决方法
如果关键应用程序发生崩溃,并且崩溃似乎是由 HotSpot VM 中的错误引起的,那么可能需要快速找到临时解决方法。本节的目的是提出一些可能的解决方法。如果使用最新版本的 Java SE 部署的应用程序发生崩溃,则应始终通过记录支持呼叫(对于具有支持合同的客户)或一次性报告崩溃来向 Sun Microsystems 报告崩溃:事件,或者通过向错误数据库提交错误。
注意 -即使本节中的解决方法成功地消除了崩溃,该解决方法也 不能解决问题,而只是临时解决方案。使用证明问题的原始配置提交支持电话或错误报告。
4.2.1 HotSpot 编译器线程或编译代码崩溃
如果致命错误日志表明崩溃发生在编译器线程中,那么您可能(但并非总是如此)遇到了编译器错误。同样,如果崩溃发生在已编译的代码中,则编译器可能生成了不正确的代码。
对于 HotSpot 客户端 VM( -client
选项),编译器线程在错误日志中显示为 CompilerThread0
. HotSpot Server VM 有多个编译器线程,这些线程在错误日志文件中显示为 CompilerThread0
、 CompilerThread1
和 AdapterThread
.
下面是 J2SE 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 客户端 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 垃圾回收时的崩溃
如果在垃圾收集 (GC) 期间发生崩溃,则致命错误日志会报告 a VM_Operation
正在进行中。出于讨论的目的,假设大多数并发的 GC ( -XX:+UseConcMarkSweep
) 未使用。显示 VM_Operation
在 THREAD
日志部分中,并指示以下情况之一:
- 用于分配的生成集合
- 全代合集
- 并行 gc 分配失败
- 并行 gc 永久分配失败
- 并行 gc 系统 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
注意 -垃圾收集期间的崩溃并不意味着垃圾收集实现中存在错误。它还可能表示编译器或运行时错误或其他一些问题。
如果您在垃圾收集期间遇到反复崩溃,您可以尝试以下解决方法:
- 切换 GC 配置。例如,如果您使用的是串行收集器,请尝试吞吐量收集器,反之亦然。
- 如果您使用的是 HotSpot Server VM,请尝试 HotSpot Client VM。
如果您不确定正在使用哪个垃圾收集器,您可以使用 jmap
Solaris OS 和 Linux 上的实用程序(请参阅 2.7 jmap
实用程序 )从核心文件中获取堆信息(如果核心文件可用)。一般来说,如果命令行中没有指定 GC 配置,那么在 Windows 上将使用串行收集器。在 Solaris OS 和 Linux 上,这取决于机器配置。如果机器至少有 2GB 内存并且至少有 2 个处理器,那么将使用吞吐量收集器(Parallel GC)。对于较小的机器,串行收集器是默认设置。选择串行收集器 -XX:+UseSerialGC
的选项是,选择吞吐量收集器的选项是 -XX:+UseParallelGC
. 如果作为一种解决方法,您从吞吐量收集器切换到串行收集器,那么您可能会在多处理器系统上遇到一些性能下降。在诊断和解决根本问题之前,这可能是可以接受的。
4.2.3 类数据共享
类数据共享是 J2SE 5.0 中的一个新特性。当使用 Sun 提供的安装程序在 32 位平台上安装 JRE 时,安装程序将系统 JAR 文件中的一组类加载到私有内部表示中,并将该表示转储到称为共享存档的文件中。当 VM 启动时,共享存档被映射到内存中。这节省了类加载,并允许与类关联的许多元数据在多个 VM 实例之间共享。在 J2SE 5.0 中,仅当使用 HotSpot 客户端 VM 时才启用类数据共享。此外,仅串行垃圾收集器支持共享。
致命错误日志在日志标题中打印版本字符串。如果启用了共享,则由文本 指示, sharing
如下例所示:
# An unexpected error has been detected by HotSpot Virtual Machine: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x08083d77, pid=3572, tid=784 # # Java VM: Java HotSpot(TM) Client VM (1.5-internal mixed mode, sharing) # Problematic frame: # V [jvm.dll+0x83d77]
-Xshare:off
可以通过在命令行上提供选项来禁用共享 。如果崩溃无法在禁用共享的情况下复制,但可以在启用共享的情况下复制,那么您可能遇到了此功能的错误。在这种情况下,收集尽可能多的信息并提交错误报告。
4.3 Microsoft Visual C++ 版本注意事项
JDK 6 软件是在 Windows 上使用 Microsoft Visual Studio .NET 2003(专业版)编译的,用于 32 位平台和 Windows Server 2003 SP1 平台 SDK - 2005 年 4 月版(用于 64 位平台)。如果您在使用 Java SE 应用程序时遇到崩溃,并且如果您有使用不同版本的编译器编译的本机或 JNI 库,那么您必须考虑运行时之间的兼容性问题。具体来说,仅当您在处理多个运行时时遵循 Microsoft 准则时,您的环境才受支持。例如,如果您使用一个运行时分配内存,那么您必须使用同一运行时释放它。如果您使用与分配资源的库不同的库释放资源,则可能会出现不可预测的行为或崩溃。