揭秘JVM创世过程之紧急制动机制-异常处理
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Java世界的紧急制动机制
在 OpenJDK 8u44 的源码中,当 Java 初始化期间(例如执行System.initializeSystemClass)抛出异常时,C++ 层感知并终止 VM 的过程并非通过 C++ 的try-catch机制,而是通过一套基于“挂起异常字段(Pending Exception)”和“CHECK 宏”的显式检查机制。
以下是这一过程的深度拆解:
1. 第一现场:JavaThread的异常槽位
在 JVM 内部,每个线程(JavaThread)都有一个专门存放异常对象的指针字段。
_pending_exception:记录Java 层的异常对象(oop)
_exception_file:记录抛出异常的 C++ 文件
_exception_line:抛出异常的行号
源码位置:下面是整理后的示例代码。
classThread:publicThreadShadow{// ...oop _pending_exception;// 指向 Java 层的异常对象(oop)constchar*_exception_file;// 抛出异常的 C++ 文件int_exception_line;// 抛出异常的行号};hotspot\src\share\vm\runtime\thread.hpp
- 原始代码
classThread:publicThreadShadow{friendclassVMStructs;private:// Exception handling// (Note: _pending_exception and friends are in ThreadShadow)//oop _pending_exception; // pending exception for current thread// const char* _exception_file; // file information for exception (debugging only)// int _exception_line; // line information for exception (debugging only)// 省略部分代码}hotspot\src\share\vm\utilities\exceptions.hpp
classThreadShadow:publicCHeapObj<mtThread>{friendclassVMStructs;protected:oop _pending_exception;// Thread has gc actions.constchar*_exception_file;// file information for exception (debugging only)int_exception_line;// line information for exception (debugging only)friendvoidcheck_ThreadShadow();// checks _pending_exception offset// 省略部分代码}当 Java 代码通过call_stub执行并抛出异常时,Java 异常处理机制(或者解释器/JIT 编译的代码)会将异常对象的引用存入这个_pending_exception字段,然后直接返回到 C++。
2. 感知机制:万能的CHECK宏
在Threads::create_vm等初始化代码中,你会看到大量的 C++ 调用带有一个特殊的宏。这是 C++ 感知 Java 异常的“神经末梢”。
- 源码位置:
src/share/vm/runtime/exceptions.hpp
#defineCHECKTHREAD);if(HAS_PENDING_EXCEPTION)return;(void)(0#defineCHECK_0THREAD);if(HAS_PENDING_EXCEPTION)return0;(void)(0#defineCHECK_NHTHREAD);if(HAS_PENDING_EXCEPTION)returnHandle();(void)(0工作原理:
每当 C++ 层调用一个可能触发 Java 代码的函数(如JavaCalls::call_static)时,紧接着就会执行if (HAS_PENDING_EXCEPTION)。这个宏会检查当前线程的_pending_exception是否为空。如果不为空,说明 Java 层出事了,C++ 函数会立即提前返回,将异常向上传递。
3. 拦截点:Threads::create_vm中的关键检查
在 VM 启动的核心函数Threads::create_vm中,JVM 会分阶段检查初始化进度。如果核心类加载或初始化失败,流程如下:
- 源码位置:
src/share/vm/runtime/thread.cpp
jintThreads::create_vm(JavaVMInitArgs*args,bool*canTryAgain){// ... 执行某项 Java 初始化任务 ...call_initialize_system_class(CHECK_0);// 如果这里抛出异常,直接返回 0// 或者显式检查if(HAS_PENDING_EXCEPTION){// 发现异常,进入收尾逻辑vm_exit_during_initialization(Handle(THREAD,PENDING_EXCEPTION));}}4. 物理栈的回溯:穿过 Call Stub
当异常发生且未在 Java 层被捕获时,逻辑会回到Call Stub。
- 现场还原:Call Stub 执行其“收尾工作”,根据之前保存的
rbp和rsp还原 C++ 的寄存器环境。 - 平滑着陆:它并不处理异常逻辑,它只是保证 CPU 能够安全地跳回到
JavaCalls::call_helper里的下一条 C++ 指令。 - 状态确认:回到 C++ 后,由于
CHECK宏的存在,当前的 C++ 函数会意识到“出事了”,从而停止后续的初始化逻辑(如停止加载其他类)。
5. 终结者:vm_exit_during_initialization
一旦 C++ 层确认在初始化这种关键时刻发生了异常,它不会尝试恢复,而是直接调用vm_exit_during_initialization。
- 源码位置:
src/share/vm/runtime/java.cpp
voidvm_exit_during_initialization(Handle exception){// 1. 打印异常堆栈,让你看到那个著名的 "Error occurred during initialization of VM"java_lang_Throwable::print_stack_trace(exception,tty);// 2. 终止 VMvm_abort(false);}这个函数会做两件事:
- 动作:
- 翻译异常:调用
java_lang_Throwable::print_stack_trace。虽然此时 Java 环境还没完全好,但 JVM 已经有足够的“硬核”能力去解析那个异常对象的 backtrace,并打印到 stderr。否则直接在 stderr 打印 C++ 侧记录的异常名。 - 停掉所有线程:通知安全点(Safepoint)机制,尝试停止其他可能正在运行的辅助线程。
- 销毁内存:释放已经分配的部分本地资源。
- 退出进程:直接调用
os::exit(1)或abort(),不给 Java 留任何挣扎的机会,强制关闭整个 JVM 进程。
- 翻译异常:调用
6. 为什么不直接用 C++ 的try-catch?
这是一个经典的架构选择真相:
- 性能与受控:C++ 异常在跨模块(尤其是在汇编生成的
call_stub和 C++ 之间)传递时开销极大且难以预测。 - 隔离性:Java 的异常是 Java 堆里的对象(
oop),而 C++ 的异常是系统级的。JVM 需要精确控制“谁抛出了异常”以及“这个异常对象在 GC 扫描时是否存活”,通过JavaThread字段来手动维护这种关系,比依赖 C++ 运行时要稳健得多。
总结
在Java世界感知异常的真相是:
架构选择真相:主动轮询
- 性能与受控:C++ 异常在跨模块(尤其是在汇编生成的
call_stub和 C++ 之间)传递时开销极大且难以预测。 - 隔离性:Java 的异常是 Java 堆里的对象(
oop),而 C++ 的异常是系统级的。JVM 需要精确控制“谁抛出了异常”以及“这个异常对象在 GC 扫描时是否存活”,通过JavaThread字段来手动维护这种关系,比依赖 C++ 运行时要稳健得多。 - Java 执行逻辑负责**“写”**异常标志(
_pending_exception)。 - C++ 调用者负责在每一行关键代码后通过
CHECK宏**“读”**异常标志。
提前布局:在进入 main 方法时的物理栈布局与异常感知
当你进入main方法时,物理栈就像一个精心布置的陷阱阵列:
- Java 栈帧(最顶端):一旦抛出异常,这里只负责填充异常信息。
- Call Stub 汇编层:作为“时空隧道”,它负责把 CPU 控制权从 Java 语义安全地交还给 C++ 语义。
- C++ 逻辑层(底层):通过
CHECK宏像接力赛一样监控JavaThread->_pending_exception字段。
从 Java 到 C++ 的感应链条
- Java 层:发生异常,将异常对象塞进
JavaThread的_pending_exception。 - 汇编层 (call_stub):清理 Java 栈帧,跳回 C++ 调用处。
- C++ 层 (JavaCalls):函数返回,通过
HAS_PENDING_EXCEPTION宏感知到异常。 - VM 核心 (create_vm):发现异常在初始化关键路径上,调用
vm_exit_during_initialization。 - OS 层:C++ 发起
exit系统调用,销毁整个物理进程。
