(伪)2022 NJUSE VJVM Lab4.1 异常处理

 

本篇 Blog 的主要内容是我对南京大学 2022 软工大作业 VJVM 的继续完善, 用 Java 实现 jvm.

前两个 Lab 代码基于学长 Github@amnore框架代码, 具体的实验指引见 https://amnore.github.io/VJVM/, 答案的视频解说见 https://space.bilibili.com/507030405, 以下内容假设你已经完成 Lab1&2.
从 Lab3 开始的代码和测试用例为我自己的补充.

前言

经过 Lab3 的”洗礼”, 我们的 jvm 已经可以处理一些基本的的面向对象问题了, 终于有了一些像 “Java” Virtual Machine 了, 但在实现面向对象特性过程中出现的异常我们暂时还没有对他们进行正确的处理.

面向对象部分还有一些繁琐的”善后工作”没有完成, 测试用例也不够完善, 但让我们先搁置一下他们(可能以后以 Lab 番外篇的形式放出), 先把重要的异常处理机制加入我们的 jvm.

Error, Exception 与 RuntimeException

在异常处理之前, 我们先来梳理一下 Java 语言中的错误种类:

  • Error

    继承自 Throwable (顾名思义, 可以被 throw)

    Error 是虚拟机内部出现的错误, 比如 StackOverflowError, OutOfMemoryError. 程序通常无法处理这些错误.

  • Exception

    继承自 Throwable

    受检异常(checked), 比如 IOException, 在编译期进行检查, 编写程序时必须显式地对受检异常进行捕获.

  • RuntimeException

    继承自 Exception (所以也间接继承了 Throwable)

    非受检异常(unchecked), 比如 NullPointerException, 不会在编译期进行检查, 由程序员自行决定是否捕获, 不处理则抛给上层.

虽然受检异常能让强迫程序编写者对异常进行处理, 但也有人认为受检异常在大型软件中没有必要存在, Kotlin 语言中就没有受检异常.

在 Java 程序中遇到一些的确没必要处理的受检异常时我们也可以将其包装成非受检异常再次抛出.

1
2
3
4
5
try {
doSomething();
} catch(Exception e) {
throw new RuntimeException(e);
}

这样既能防止异常被直接忽略导致难以排查错误, 又不需要在每层调用中写大量处理受检异常的代码.

在之前我们的 jvm 实现中我们也使用了 lombok 库中的 @SneakyThrows 注解也能帮我们免去上述这种模板型代码, 使代码更简洁.

jvm 的异常处理实现 - 异常表

Code 这个 Attribute 中有一个 exceptionHandlers 的结构, 我们之前解析的过程中只是忽略了这部分的值, 并没有处理.

其中存储的是异常的 Handler 的相关信息, 我们现在需要将其按照文档的说明解析出来.

jvm 的异常处理相关指令

异常处理最重要的指令就是 athrow, 即抛出一个异常(继承于 java.lang.Throwable 的一个实例 objref).

如果有异常抛出会先在这个 methodCode 中寻找合适的 Handler, 如果没有合适的 Handler 则抛给上层调用者.

具体的实现请仔细阅读 JVM 手册中关于 athrow 指令异常处理的相关说明.

除了 athrow 抛出的异常, 不要忘记补全之前其他指令中提到的会抛出异常的场景. (比如 NPE, ArithmeticException, ClassCastException 等.)

BTW: Java 受检异常的机制是一种编译时(compile time)检查, 而我们实现的 JVM 是 Java Bytecode 的运行环境(runtime), 我们并不需要考虑受检异常的处理, 只需要实现 Bytecode 相应的行为即可.

实现细节

由于 Java 中的异常需要继承自 Throwable, Throwable 中调用了许多其他方法. (可以使用 IDE 查看 Throwable 中的源代码, javap 指令查看编译后的字节码.)

所以你通过了 Lab3 中的简单测试样例只能代表你错的不太离谱, 但当执行 Throwable 中的指令时可能触发更多未知的问题.

Debug 建议: 在 JVM Specs 中提到 must be xxx 的部分, 使用 assert 对其进行判断, 做到 fail-fast, 可以减轻很多 Debug 负担.

同时, Throwable 中涉及了很多 native 方法, 这部分代码在执行时会报错, 在我们实现本地方法调用之前, 我们可以模仿之前调用 IOUtil 时的做法, 先在 nativeTable 中添加对应的方法并自行实现.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/njuse-jvm-lab4.1/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬