专家视角看异常处理机制之栈帧回溯实现机制


前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

异常处理机制

JVM 处理异常并不只是简单的 if-else,它涉及字节码指令、数据结构以及复杂的栈回溯。将从以下三个核心维度进行分析。本文中主要介绍栈帧回溯机制。

  1. 异常表 (Exception Table):看看编译器如何在字节码里给异常设下“埋伏”。参见:专家视角看异常处理机制之异常表
  2. 栈帧回溯 (Stack Unwinding):研究异常是如何在方法调用链中传递的。
  3. Finally 的秘密:拆解为什么 finally 块具有“绝对执行”的魔力。

栈帧回溯 (Stack Unwinding)

理解栈帧回溯(Stack Frame Unwinding)是掌握 JVM 运行时异常分发逻辑的核心。当一个方法抛出异常,且当前方法的异常表无法处理该异常时,JVM 必须销毁当前帧并回到调用者(Caller)的上下文中继续寻找处理器。
这一过程在 OpenJDK 8的 HotSpot 虚拟机中涉及复杂的 C++ 运行时逻辑、寄存器状态恢复以及栈遍历(Stack Walking)。

1. 核心抽象:frameRegisterMap

当异常发生且当前方法无法处理时,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 内部产生硬件异常(如 SIGSEGVNullPointerException)时,执行流会进入运行时库。对于编译后的代码(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_maptrueframe::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 源码实现中,我们可以得出关于栈帧回溯的几个核心技术点:

  1. 开销来源:回溯的开销不仅在于指针移动,更在于对 CodeCache 的高频查找RegisterMap 的同步以及 内联元数据(ScopeDesc)的解压
  2. 异步代价:由于异常打破了预取指令缓存(Prefetch)和流水线,频繁回溯会导致明显的 CPU 周期浪费。
  3. 安全性保障:回溯过程中,JVM 必须通过 HandleMark 保护正在被回溯的栈帧中的对象不被 GC 错误回收,这增加了垃圾回收器与运行时系统的耦合度。

这种机制确保了 Java 语言在保证类型安全和结构化控制流的同时,能够动态地反映高度优化(内联)后的实际执行路径。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值