当前位置: 首页 > news >正文

第十一章 用Java实现JVM之异常处理

用Java实现JVM目录

第零章 用Java实现JVM之随便说点什么
第一章 用Java实现JVM之JVM的准备知识
第二章 用Java实现JVM之命令行工具
第三章 用Java实现JVM之查找Class文件
第四章 用Java实现JVM之解析class文件
第五章 用Java实现JVM之运行时数据区
第六章 用Java实现JVM之指令集和解释器
第七章 用Java实现JVM之类和对象
第八章 用Java实现JVM之方法调用和返回
第九章 用Java实现JVM之数组和字符串
第十章 用Java实现JVM之本地方法调用
第十一章 用Java实现JVM之异常处理
第十二章 用Java实现JVM之结束


文章目录

  • 用Java实现JVM目录
  • 前言
  • 异常
      • 一、错误(Error)
      • 二、异常(Exception)
          • 1. **受检异常(Checked Exception)**
          • 2. **非受检异常(Unchecked Exception)**
  • 代码实现
          • 异常处理表
          • `athrow`指令
          • 异常打印
  • 测试
  • 总结


前言

    上一篇我们已经实现了本地方法,今天开启新的征程,继续往下。聚焦于异常处理

异常

    从大的方向来看,Java 在运行过程中可能遇到的问题,主要可以划分为两大类:错误(Error)异常(Exception)。这两者虽然在表现上可能都是程序运行中断,但它们的含义、产生原因、处理方式却截然不同。


一、错误(Error)

    错误 通常指的是一些 严重的系统级问题,这些问题非程序本身能处理,一旦出现,基本就意味着 JVM 自身出了问题,程序无法继续运行。例如:

  • OutOfMemoryError:内存溢出,JVM 无法为对象分配内存

  • StackOverflowError:栈空间溢出,通常是无限递归调用导致

  • VirtualMachineError:虚拟机内部错误,如 JIT 编译器 崩溃

    这类错误并不推荐用 try-catch 来捕获,也不建议也不可能完全恢复,更像是一种“致命信号”:程序可能已经无法继续安全运行。通常我们只能记录日志,或通知运维重启服务,根本原因往往是代码设计或配置不当,或者系统资源不足


二、异常(Exception)

    相比之下,异常 则是 应用级别的问题,指程序运行过程中可预期、可以被捕获和处理的问题。它们一般不会导致整个 JVM 崩溃,但需要程序员显式处理。例如:

  • NullPointerException:空指针异常。

  • IOException:输入输出异常,文件或网络读取失败。

  • SQLException:数据库操作时出错。

    Java 中,异常又被进一步分为两类:

1. 受检异常(Checked Exception)
  • 特点:必须显示处理(编译期检查),否则编译器报错
  • 常见场景:文件操作、数据库连接、网络通信等
  • 例如:IOExceptionSQLException
  • 解决方式:用 try-catch 捕获,或者用 throws 抛出
2. 非受检异常(Unchecked Exception)
  • 特点:运行时才出现,不强制处理(编译期不报错)
  • 通常是程序逻辑上的错误,如访问空对象、数组越界等
  • 例如:NullPointerExceptionIndexOutOfBoundsException
  • 解决方式:可以用 try-catch 捕获,也可以通过加强逻辑判断来避免

代码实现

    梳理完 Java 的异常体系后,回到现实的问题,JVM 要如何处理异常?在日常开发出,我们可能抛出来一个或多个异常,出现异常后也会打印出来报错的行数。异常发生时程序需要做什么,不妨再深入想一想:

  • 当异常抛出时,它具体从哪里“冒出来”?
  • 它是怎么被虚拟机捕获的?
  • 处理异常的代码又是怎么被找到的?

    回想平时用到的 try-catch,它本质上就是一段告诉虚拟机: “如果在这段代码里出现了某种异常,跳转到这里处理它”。但虚拟机又是如何知道这其中的对应关系?经过前面一系列的洗礼,一旦出现多个XX,基本上都会存在一张表。异常也不例外。字节码文件里藏着一张“异常表”(Exception Table),这张表详细记录了每个 try-catch 的起止位置,以及能处理哪些异常类型。当执行到某条指令时,如果抛出了异常,虚拟机会查这张表,看看当前指令是否在某个 try 区间内;如果在,再根据异常类型找到对应的catch 块。如果找到,就调整程序计数器,跳转到对应的 catch 代码继续执行。如果找不到,说明当前方法不能处理,虚拟机就会弹出当前调用栈,回到调用它的方法,重复这个查找过程。直到找到处理代码,或者调用栈空了,异常传递无果,虚拟机才会把异常打印出来

    这样一来,异常的捕获与处理就像在字节码里有一个“地图”,告诉虚拟机:“遇到异常,去这里走。”同时,为了让错误提示更友好,定位到具体的源码行号,字节码中还有专门的“行号表”(LineNumberTable),将字节码指令的偏移量对应到源码的行号。当异常发生,虚拟机结合异常抛出的指令地址和行号表,就能告诉你报错的代码行在哪里。因为源代码中,很多东西是给人看的,不是给虚拟机看的。所以在编译的过程中会过滤掉这些,导致字节码的行号和源代码的行号对不上


这就是 JVM 规范 对异常处理的基本设计:

  • 异常表(Exception Table)
  • 行号表(LineNumberTable)
  • 程序计数器(PC)用于定位当前执行指令
  • 调用栈帧的弹出与跳转机制
+-----------+----------+-------------+------------+
| start_pc  | end_pc   | handler_pc  | catch_type |
+-----------+----------+-------------+------------+
|    0      |   20     |     30      |  NumberFormatException |
+-----------+----------+-------------+------------+
异常处理表

    巴拉了这么多,先把字节码中的 异常处理表 解析出来再说。JMethod添加如下代码:

	private List<JMethodExceptionTable> catchExceptionTable;private List<JClassRef> throwExceptionTable;@Overrideprotected void newDescInfo(AttributeInfo attr, JClass jClass, List<ConstantPool> constantPool) {if (attr instanceof Code) {Code code = (Code) attr;this.setCode(code.getInstructionCode());this.setMaxStack(code.getMaxStack());this.setMaxLocals(code.getMaxLocals());resolveExceptionTable(code, constantPool);resolveAttrs(code, constantPool);} else if (attr instanceof Exceptions) {this.resolveExceptions((Exceptions) attr, constantPool);} else if (attr instanceof RuntimeVisibleParameterAnnotations) {RuntimeVisibleParameterAnnotations paramAnno = (RuntimeVisibleParameterAnnotations) attr;this.paramAnnos = paramAnno.getByteCodes();}}private void resolveExceptions(Exceptions ex, List<ConstantPool> constantPool) {List<Integer> indexList = ex.getExceptionIndexTable();if (CollectionUtils.isNotEmpty(indexList)) {for (Integer index : indexList) {JClassRef jClassRef = (JClassRef) constantPool.get(index).getVal();this.throwExceptionTable.add(jClassRef);}}}
athrow指令

    代码上的功能,对应到虚拟机都是由指令来完成。异常也是一样的。抛出异常的指令为athrow。整个指令的功能就是根据栈顶的异常对象,去异常表进行匹配。RefInstruction#execute()改动如下:

	case ATHROW: {//抛出异常信息JObject ref = popOperandStackRefVal();if (ref == null) {throw new NullPointerException();}while (!jThread.getJvmStack().isEmpty()) {//回到上一个指令int pc = jThread.getNextPc().get() - 1;int handlerPC = getJMethod().findExceptionHandler(ref.getJClass(), pc);if (handlerPC != -1) {getStackFrame().getOperandStack().getStack().clear();offerIncrement(handlerPC - 1);break;}jThread.getJvmStack().pop();}if (jThread.getJvmStack().isEmpty()) {JField jField = ref.getJClass().getJField("detailMessage", "Ljava/lang/String;");String msg = new String((char[]) ((JObject) (((JObject) ref.getField(jField.getSlotId()).getVal())).getFields()[0].getVal()).getData());System.err.println(ref.getJClass().getClassName() + ": " + msg);end();}break;}
异常打印

    异常信息的打印还需要本地方法支持。新增JThrowableNativeRegistry类,代码如下:

public class JThrowableNativeRegistry extends JNativeRegistry {private static final JThrowableNativeRegistry instance = new JThrowableNativeRegistry();static {registry("java/lang/Throwable", "fillInStackTrace", "(I)Ljava/lang/Throwable;", JThrowableNativeRegistry::fillInStackTrace);}protected static void fillInStackTrace(JThread jThread) {JObject ref = jThread.getJvmStack().getTop().getLocalVars().getRefVal(0);jThread.getJvmStack().pushOperandStackRefVal(ref);ref.setExtra(createStackTraceElements(ref, jThread));}protected static JStackTraceElement[] createStackTraceElements(JObject exJObject, JThread jThread) {int skip = distanceToObject(exJObject.getJClass()) + 2;StackFrame[] sfs = new StackFrame[jThread.getJvmStack().size() - skip];JStackTraceElement[] jstes = new JStackTraceElement[sfs.length];for (int i = 0; i < sfs.length; i++) {StackFrame sf = jThread.getJvmStack().elementAt(sfs.length - i - 1);JClass jc = sf.getJClass();JMethod jm = sf.getJMethod();JStackTraceElement ste = new JStackTraceElement(jc.getSourceFile(), jc.getClassName(), jm.getName(), jm.getLineNumber(sf.getReturnAddress().get() - 1));jstes[i] = ste;}return jstes;}private static int distanceToObject(JClass exJClass) {int distance = 0;for (JClass jc = exJClass.getSuperClass(); jc != null; jc = jc.getSuperClass()) {distance++;}return distance;}public static JThrowableNativeRegistry getInstance() {return instance;}/*** 虚拟机栈信息*/@Getter@Setter@NoArgsConstructor@AllArgsConstructorstaticclass JStackTraceElement {private String fileName;private String className;private String methodName;private Integer lineNumber;}
}

测试

    ok,接下来进入测试环节,验证今天的努力成果。新增ParseIntTest类,代码如下:

public class ParseIntTest {public static void main(String[] args) {foo(args);}private static void foo(String[] args) {try {bar(args);} catch (NumberFormatException e) {System.out.println(e.getMessage());}}private static void bar(String[] args) {if (args.length == 0) {throw new IndexOutOfBoundsException("no args!");}int x = Integer.parseInt(args[0]);System.out.println(x);}
}

在添加一个测试类,ExTest代码如下:

public class ExTest {public static void main(String[] args) {CmdCommand cmdCommand = new CmdCommand();cmdCommand.parseCmd(args);}
}

idea增加两个启动类,ExTest-normal配置如下:

-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch10\target\test-classes" com.hqd.jjvm.ex.ParseIntTest aa bb

在这里插入图片描述

idea增加两个启动类,ExTest-ex配置如下:

-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch10\target\test-classes" com.hqd.jjvm.ex.ParseIntTest

在这里插入图片描述

测试结果如下:

在这里插入图片描述

在这里插入图片描述

有参数的顺利打印出来参数,无参数的抛出了异常


总结

    今天主要讲述异常处理,整个 JVM 的实现进度已经进入尾声了。。。

http://www.lryc.cn/news/596699.html

相关文章:

  • C++11--锁分析
  • 华为视觉算法面试30问全景精解
  • What Does “Directory of the Script Being Run” Mean?
  • final修饰符不可变的底层
  • SpringBoot PO VO BO POJO实战指南
  • Pycharm下载、安装及配置
  • 力扣 hot100 Day52
  • RabbitMQ03——面试题
  • 为什么要微调大语言模型
  • 论文笔记 | Beyond Pick-and-Place: Tackling Robotic Stacking of Diverse Shapes
  • 解决pip指令超时问题
  • 数据结构 堆(2)---堆的实现
  • LeetCode 热题100:42.接雨水
  • Unity UI的未来之路:从UGUI到UI Toolkit的架构演进与特性剖析(1)
  • 业务流逻辑如何搭建?为何橙武平台选用了 LogicFlow?​
  • day19 链表
  • 程序是如何生成的-以c语言为例
  • 信息学奥赛一本通 1553:【例 2】暗的连锁
  • 前端_CSS复习
  • 【React 入门系列】React 组件通讯与生命周期详解
  • 高可用架构模式——数据集群和数据分区
  • 单细胞转录组学+空间转录组的整合及思路
  • OneCode3.0 UI组件注解详解手册
  • 【vscode】vscode中python虚拟环境的创建
  • 回调地狱及解决方法
  • error C++17 or later compatible compiler is required to use ATen.
  • 【coze扣子】第1篇:coze快速入门
  • 威胁情报:Solana 开源机器人盗币分析
  • 以Java程序员角度理解MCP
  • 学习游戏制作记录(战斗系统简述以及击中效果)7.22