PHP异常对象由Zend引擎创建zend_exception结构体,含message、code等字段并标记栈中断;throw触发引擎级控制流切换,catch按编译期映射表匹配且仅限当前函数帧,finally通过插入所有退出路径opcode实现无条件执行。
抛出 throw new Exception() 时,PHP不会简单地新建一个PHP对象,而是由Zend引擎底层分配一个 zend_exception 结构体。这个结构体不是纯PHP层的 Exception 实例,而是一个C语言结构,包含 message、code、file、line 和 trace 等字段,还持有指向原始调用栈(zend_backtrace)的指针。
关键点在于:这个结构体一创建,当前执行栈就立刻被标记为“中断”,后续字节码不再执行——不是靠PHP代码跳转,而是引擎级控制流中断。
zend_exception 是真实参与异常传播的实体,PHP层的 Exception 对象只是它的用户态封装catch 捕获,这个结构体会一路向上回溯调用栈,每层都检查是否有匹配的 catch 指令(opcode ZEND_CATCH)PHP的 try/catch 不是“作用域查找”,而是编译期生成的指令映射表。每个 try 块在编译后会记录起始和结束opcode位置,以及对应 catch 的跳转地址;这个映射只对当前函数有效。
也就是说,异常从子函数抛出后,引擎只会检查**当前函数帧**有没有能匹配的 catch,不会自动“穿透”到上层函数去扫描它们的 try 块——除非上层显式写了 try 并且该函数调用被包裹其中。
立即学习“PHP免费学习笔记(深入)”;
throw,父函数没写 try → 异常直接上抛,不隐式捕获try,但 catch 类型不匹配(比如抛 PDOException 却只 catch (Exception))→ 仍上抛,不降级匹配catch 块按源码顺序线性匹配,引擎不做类型继承关系推导,只做指针比对(是否是同一类或其子类)finally 不是靠运行时判断“有没有异常”,而是在编译阶段就把它的opcode插入到所有可能退出路径的末尾:包括正常return、throw、exit(),甚至goto跳转——只要退出当前函数,就会先执行 finally 块。
这导致一个容易忽略的行为:如果 finally 里有 return 或再次 throw,它会覆盖前面 try 或 catch 中的返回值或异常。
finally 在Zend VM中对应独立的opcode序列,与 try/catch 解耦catch 块里 throw 新异常,finally 仍会先执行完,再抛新异常finally,旧版本只能靠手动 register_shutdown_function() 模拟,但无法拦截 exit()
当异常一路向上找不到任何 catch,Zend引擎会终止当前请求,并把控制权交给全局异常处理器。默认行为是输出堆栈并终止脚本,但你可以用 set_exception_handler() 替换它。
注意:set_exception_handler() 只接管未被捕获的 Exception 和 Error(PHP 7+),但**不接管传统PHP错误**(如 E_WARNING),那些还得靠 set_error_handler()。
Throwable 对象,不是字符串display_errors 和 SAPI 层处理逻辑throw 那行,而是异常在调用栈中“静默穿越”多层函数时,哪一层意外吞掉了它,或者 finally 里悄悄改写了返回结果。