栈帧回溯
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
异常处理机制
JVM 处理异常并不只是简单的 if-else,它涉及字节码指令、数据结构以及复杂的栈回溯。将从以下三个核心维度进行分析。本文中主要介绍栈帧回溯机制。
- 异常表 (Exception Table):看看编译器如何在字节码里给异常设下“埋伏”。参见:专家视角看异常处理机制之异常表
- 栈帧回溯 (Stack Unwinding):研究异常是如何在方法调用链中传递的。
- Finally 的秘密:拆解为什么
finally块具有“绝对执行”的魔力。
栈帧回溯 (Stack Unwinding)
理解栈帧回溯(Stack Frame Unwinding)是掌握 JVM 运行时异常分发逻辑的核心。当一个方法抛出异常,且当前方法的异常表无法处理该异常时,JVM 必须销毁当前帧并回到调用者(Caller)的上下文中继续寻找处理器。
这一过程在 OpenJDK 8的 HotSpot 虚拟机中涉及复杂的 C++ 运行时逻辑、寄存器状态恢复以及栈遍历(Stack Walking)。
1. 核心抽象:frame 与 RegisterMap
当异常发生且当前方法无法处理时,JVM 必须执行一套复杂的动态逻辑:它需要通过物理栈逐层“向上”寻找调用者,恢复寄存器状态,并识别出对应的逻辑帧。在 OpenJDK 8中,这一机制主要实现在 HotSpot 运行时的 frame 类及其关联的 RegisterMap 中。
在 HotSpot 中,栈帧回溯的核心不在于“删除”内存,而在于状态的恢复与跳转。
-
frame类 (hotspot\src\share\vm\runtime\frame.hpp):它是对物理栈帧的封装,包含了指向调用者帧的指针、当前的程序计数器(PC)和栈指针(SP)。
在 HotSpot 源码中,栈帧的物理表示由frame类(frame.hpp)定义。它是一个轻量级对象,封装了当前线程栈中的关键指针。核心指针定义
在 x86 架构下,一个栈帧主要由以下三个指针定义:
_sp(Stack Pointer): 栈顶指针。_pc(Program Counter): 当前执行的指令地址或返回地址。_fp(Frame Pointer): 栈底指针(通常在解释模式下有效)。
hotspot\src\share\vm\runtime\frame.hpp中_sp、_pc的定义。
class frame VALUE_OBJ_CLASS_SPEC {
private:
// Instance variables:
intptr_t* _sp; // stack pointer (from Thread::last_Java_sp)
address _pc; // program counter (the next instruction after the call)
CodeBlob* _cb; // CodeBlob that "owns" pc
}
hotspot\src\cpu\x86\vm\frame_x86.hpp中_fp的定义。
private:
// an additional field beyond _sp and _pc:
intptr_t* _fp; // frame pointer
public:
// Constructors
frame(intptr_t* sp, intptr_t* fp, address pc);
frame(intptr_t* sp, intptr_t* unextended_sp, intptr_t* fp, address pc);
frame(intptr_t* sp, intptr_t* fp);
RegisterMap(hotspot\src\share\vm\runtime\registerMap.hpp):回溯过程中最重要的辅助工具。它记录了在栈遍历过程中,各个寄存器(如栈底指针、被调用者保存寄存器)在栈中的存储位置。没有它,回溯后 CPU 无法恢复到调用者执行时的寄存器状态。
2. 栈帧回溯的核心入口:分发与搜索
当 Java 代码执行 athrow 字节码或 JVM 内部产生硬件异常(如 SIGSEGV 转 NullPointerException)时,执行流会进入运行时库。对于编译后的代码(C1/C2),入口通常是 SharedRuntime::exception_handler_for_return_address。(hotspot\src\share\vm\runtime\sharedRuntime.hpp)
回溯的本质是依靠 frame 类(物理帧)和 vframe(虚拟帧)的迭代。
核心源码:查找异常处理器
在 hotspot\src\share\vm\runtime\sharedRuntime.cpp 中,JVM 寻找处理器的逻辑反映了回溯的启动:
JRT_LEAF(address, SharedRuntime::exception_handler_for_return_address(JavaThread* thread, address return_address))
return raw_exception_handler_for_return_address(thread, return_address);
JRT_END
address SharedRuntime::raw_exception_handler_for_return_address(JavaThread* thread, address return_address) {
// 1. 根据当前异常发生的返回地址寻找对应的 nmethod (编译后的代码块)
CodeBlob* blob = CodeCache::find_blob(return_address);
nmethod* nm = (blob != NULL) ? blob->as_nmethod_or_null() : NULL;
if (nm != NULL) {
// Set flag if return address is a method handle call site.
thread->set_is_method_handle_return(nm->is_method_handle_return(return_address));
if (nm->is_deopt_pc(return_address)) {
// If we come here because of a stack overflow, the stack may be
// unguarded. Reguard the stack otherwise if we return to the
// deopt blob and the stack bang causes a stack overflow we
// crash.
bool guard_pages_enabled = thread->stack_yellow_zone_enabled();
if (!guard_pages_enabled) guard_pages_enabled = thread->reguard_stack();
assert(guard_pages_enabled, "stack banging in deopt blob may cause crash");
return SharedRuntime::deopt_blob()->unpack_with_exception();
} else {
return nm->exception_begin();
}
}
// Entry code
if (StubRoutines::returns_to_call_stub(return_address)) {
return StubRoutines::catch_exception_entry();
}
// Interpreted code
if (Interpreter::contains(return_address)) {
return Interpreter::rethrow_exception_entry();
}
return NULL;
}
3. 物理回溯的实现:frame::sender
回溯中最关键的步骤是 找到“发送者”(Sender),即调用当前方法的上级方法。在 x86 架构下,这涉及对 CPU 寄存器(如 $RBP 和 $RSP)的恢复。
源码分析:frame_x86.cpp
物理回溯的具体实现在 hotspot\src\cpu\x86\vm\frame_x86.cpp。JVM 需要根据当前帧的类型(解释执行或编译执行)来计算上一个帧的位置。
frame frame::sender(RegisterMap* map) const {
// Default is we done have to follow them. The sender_for_xxx will
// update it accordingly
map->set_include_argument_oops(false);
if (is_entry_frame()) return sender_for_entry_frame(map);
if (is_interpreted_frame()) return sender_for_interpreter_frame(map);
if (_cb != NULL) {
return sender_for_compiled_frame(map);
}
// Must be native-compiled frame, i.e. the marshaling code for native
// methods that exists in the core system.
return frame(sender_sp(), link(), sender_pc());
}
frame frame::sender_for_entry_frame(RegisterMap* map) const {
// Java frame called from C; skip all C frames and return top C
// frame of that chunk as the sender
JavaFrameAnchor* jfa = entry_frame_call_wrapper()->anchor();
map->clear();
if (jfa->last_Java_pc() != NULL ) {
frame fr(jfa->last_Java_sp(), jfa->last_Java_fp(), jfa->last_Java_pc());
return fr;
}
frame fr(jfa->last_Java_sp(), jfa->last_Java_fp());
return fr;
}
frame frame::sender_for_interpreter_frame(RegisterMap* map) const {
// SP is the raw SP from the sender after adapter or interpreter
// extension.
intptr_t* sender_sp = this->sender_sp();
// This is the sp before any possible extension (adapter/locals).
intptr_t* unextended_sp = interpreter_frame_sender_sp();
return frame(sender_sp, unextended_sp, link(), sender_pc());
}
frame frame::sender_for_compiled_frame(RegisterMap* map) const {
// frame owned by optimizing compiler
intptr_t* sender_sp = unextended_sp() + _cb->frame_size();
intptr_t* unextended_sp = sender_sp;
// On Intel the return_address is always the word on the stack
address sender_pc = (address) *(sender_sp-1);
// This is the saved value of EBP which may or may not really be an FP.
// It is only an FP if the sender is an interpreter frame (or C1?).
intptr_t** saved_fp_addr = (intptr_t**) (sender_sp - frame::sender_sp_offset);
if (map->update_map()) {
// Tell GC to use argument oopmaps for some runtime stubs that need it.
// For C1, the runtime stub might not have oop maps, so set this flag
// outside of update_register_map.
map->set_include_argument_oops(_cb->caller_must_gc_arguments(map->thread()));
if (_cb->oop_maps() != NULL) {
OopMapSet::update_register_map(this, map);
}
// Since the prolog does the save and restore of EBP there is no oopmap
// for it so we must fill in its location as if there was an oopmap entry
// since if our caller was compiled code there could be live jvm state in it.
update_map_with_saved_link(map, saved_fp_addr);
}
return frame(sender_sp, unextended_sp, *saved_fp_addr, sender_pc);
}
4. 状态恢复的关键:RegisterMap
在回溯过程中,仅仅移动指针是不够的。JIT 编译器可能会将变量存储在寄存器中。如果回溯时不恢复这些寄存器的值,上层方法的执行环境就会被破坏。
RegisterMap 负责在遍历栈帧时更新寄存器的快照。如果在回溯时 update_map 为 true,frame::sender 会根据当前方法的 栈映射(Stack Map) 恢复被调用者保存(Callee-saved)的寄存器。
hotspot\src\share\vm\runtime\registerMap.hpp
class RegisterMap : public StackObj {
public:
typedef julong LocationValidType;
enum {
reg_count = ConcreteRegisterImpl::number_of_registers,
location_valid_type_size = sizeof(LocationValidType)*8,
location_valid_size = (reg_count+location_valid_type_size-1)/location_valid_type_size
};
private:
intptr_t* _location[reg_count]; // Location of registers (intptr_t* looks better than address in the debugger)
LocationValidType _location_valid[location_valid_size];
bool _include_argument_oops; // Should include argument_oop marked locations for compiler
JavaThread* _thread; // Reference to current thread
bool _update_map; // Tells if the register map need to be
// updated when traversing the stack
};
5. 虚拟回溯:处理方法内联 (Inlining)
需要注意的是 内联带来的回溯复杂性。JIT 可能会将多个 Java 方法内联成一个物理栈帧。此时,物理上的 frame::sender 一次跳过多个 Java 方法。为了获取真实的 Java 调用链,JVM 使用了 vframe。
源码分析:虚拟帧遍历
在 hotspot\src\share\vm\runtime\vframe.cpp 中,vframeStream 提供了一个逻辑视图,它能感知到一个物理帧内部被压缩的多个虚拟帧:
inline bool vframeStreamCommon::fill_from_frame() {
// Interpreted frame
if (_frame.is_interpreted_frame()) {
fill_from_interpreter_frame();
return true;
}
// Compiled frame
if (cb() != NULL && cb()->is_nmethod()) {
if (nm()->is_native_method()) {
// Do not rely on scopeDesc since the pc might be unprecise due to the _last_native_pc trick.
fill_from_compiled_native_frame();
} else {
PcDesc* pc_desc = nm()->pc_desc_at(_frame.pc());
int decode_offset;
if (pc_desc == NULL) {
JavaThreadState state = _thread->thread_state();
if (state == _thread_in_Java ) {
fill_from_compiled_native_frame();
return true;
}
decode_offset = DebugInformationRecorder::serialized_null;
} else {
decode_offset = pc_desc->scope_decode_offset();
}
fill_from_compiled_frame(decode_offset);
}
return true;
}
// End of stack?
if (_frame.is_first_frame() || (_stop_at_java_call_stub && _frame.is_entry_frame())) {
_mode = at_end_mode;
return true;
}
return false;
}
void next() {
// handle frames with inlining
if (_mode == compiled_mode && fill_in_compiled_inlined_sender()) return;
// handle general case
do {
_frame = _frame.sender(&_reg_map);
} while (!fill_from_frame());
}
6. 堆栈轨迹的填充:fillInStackTrace
这是开发者最常接触的回溯场景。当执行 new Throwable() 时,JVM 会记录当前的快照。
源码分析:
在 hotspot\src\share\vm\classfile\javaClasses.cpp 中,JVM 通过以下逻辑捕获栈帧:
void java_lang_Throwable::fill_in_stack_trace(Handle throwable, methodHandle method, TRAPS) {
if (!StackTraceInThrowable) return;
ResourceMark rm(THREAD);
// Start out by clearing the backtrace for this object, in case the VM
// runs out of memory while allocating the stack trace
set_backtrace(throwable(), NULL);
// 省略部分代码
int max_depth = MaxJavaStackTraceDepth;
JavaThread* thread = (JavaThread*)THREAD;
BacktraceBuilder bt(CHECK);
// If there is no Java frame just return the method that was being called
// with bci 0
if (!thread->has_last_Java_frame()) {
if (max_depth >= 1 && method() != NULL) {
bt.push(method(), 0, CHECK);
set_backtrace(throwable(), bt.backtrace());
}
return;
}
// 省略部分代码
int total_count = 0;
RegisterMap map(thread, false);
int decode_offset = 0;
nmethod* nm = NULL;
bool skip_fillInStackTrace_check = false;
bool skip_throwableInit_check = false;
bool skip_hidden = !ShowHiddenFrames;
for (frame fr = thread->last_frame(); max_depth != total_count;) {
Method* method = NULL;
int bci = 0;
// Compiled java method case.
if (decode_offset != 0) {
DebugInfoReadStream stream(nm, decode_offset);
decode_offset = stream.read_int();
method = (Method*)nm->metadata_at(stream.read_int());
bci = stream.read_bci();
} else {
if (fr.is_first_frame()) break;
address pc = fr.pc();
if (fr.is_interpreted_frame()) {
intptr_t bcx = fr.interpreter_frame_bcx();
method = fr.interpreter_frame_method();
bci = fr.is_bci(bcx) ? bcx : method->bci_from((address)bcx);
fr = fr.sender(&map);
} else {
CodeBlob* cb = fr.cb();
// HMMM QQQ might be nice to have frame return nm as NULL if cb is non-NULL
// but non nmethod
fr = fr.sender(&map);
if (cb == NULL || !cb->is_nmethod()) {
continue;
}
nm = (nmethod*)cb;
if (nm->method()->is_native()) {
method = nm->method();
bci = 0;
} else {
PcDesc* pd = nm->pc_desc_at(pc);
decode_offset = pd->scope_decode_offset();
// if decode_offset is not equal to 0, it will execute the
// "compiled java method case" at the beginning of the loop.
continue;
}
}
}
// 省略部分代码
// the format of the stacktrace will be:
// - 1 or more fillInStackTrace frames for the exception class (skipped)
// - 0 or more <init> methods for the exception class (skipped)
// - rest of the stack
if (!skip_fillInStackTrace_check) {
if ((method->name() == vmSymbols::fillInStackTrace_name() ||
method->name() == vmSymbols::fillInStackTrace0_name()) &&
throwable->is_a(method->method_holder())) {
continue;
}
else {
skip_fillInStackTrace_check = true; // gone past them all
}
}
if (!skip_throwableInit_check) {
assert(skip_fillInStackTrace_check, "logic error in backtrace filtering");
// skip <init> methods of the exception class and superclasses
// This is simlar to classic VM.
if (method->name() == vmSymbols::object_initializer_name() &&
throwable->is_a(method->method_holder())) {
continue;
} else {
// there are none or we've seen them all - either way stop checking
skip_throwableInit_check = true;
}
}
if (method->is_hidden()) {
if (skip_hidden) continue;
}
bt.push(method, bci, CHECK);
total_count++;
}
// Put completed stack trace into throwable object
set_backtrace(throwable(), bt.backtrace());
}
总结:深度观察
从 OpenJDK 源码实现中,我们可以得出关于栈帧回溯的几个核心技术点:
- 开销来源:回溯的开销不仅在于指针移动,更在于对
CodeCache的高频查找、RegisterMap的同步以及 内联元数据(ScopeDesc)的解压。 - 异步代价:由于异常打破了预取指令缓存(Prefetch)和流水线,频繁回溯会导致明显的 CPU 周期浪费。
- 安全性保障:回溯过程中,JVM 必须通过
HandleMark保护正在被回溯的栈帧中的对象不被 GC 错误回收,这增加了垃圾回收器与运行时系统的耦合度。
这种机制确保了 Java 语言在保证类型安全和结构化控制流的同时,能够动态地反映高度优化(内联)后的实际执行路径。

375

被折叠的 条评论
为什么被折叠?



