This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.
The main content of this blog is my further improvement on Nanjing University's 2022 SE assignment VJVM, implementing a JVM in Java.
The code for the first two labs is based on the framework code from senior student Github@amnore, available at VJVM-public. For detailed lab instructions, visit https://amnore.github.io/VJVM/, and for video explanations of the solutions, check out https://space.bilibili.com/507030405. The following assumes you have already completed Lab1 and Lab2.
Starting from Lab3, the code and test cases are my own additions.
Preface
After the “trial by fire” of Lab3, our JVM can now handle some basic object-oriented features and finally starts to feel a bit more like a “Java” Virtual Machine. However, we haven’t yet properly handled the exceptions that arose during the implementation of these object-oriented features.
There are still some tedious “cleanup tasks” left unfinished in the object-oriented part, and our test cases are also not yet comprehensive. But let’s put those aside for now (perhaps to be addressed later as an “Extra Lab” episode), and focus first on adding the crucial exception handling mechanism to our JVM.
Error, Exception, and RuntimeException
Before diving into exception handling, let’s first clarify the types of errors in Java:
Error
Inherits from
Throwable(as the name suggests, it can be thrown).Errors are internal errors within the JVM, such as
StackOverflowErrororOutOfMemoryError. Programs usually cannot recover from these errors.Exception
Also inherits from
Throwable.These are checked exceptions, such as
IOException, which are checked at compile time. When writing code, you must explicitly catch or declare checked exceptions.RuntimeException
Inherits from
Exception(and thus indirectly fromThrowable).These are unchecked exceptions, such as
NullPointerException. They are not checked at compile time, and it’s up to the programmer to decide whether to catch them. If not handled, they propagate up the call stack.
Although checked exceptions force developers to handle potential errors, some argue that they are unnecessary in large-scale software—Kotlin, for instance, does not have checked exceptions.
In Java, when encountering a checked exception that truly doesn’t need handling, we can wrap it in an unchecked exception and re-throw it.
1 | try { |
This way, we avoid silently ignoring exceptions (which makes debugging hard), while also avoiding writing repetitive exception-handling code at every call level.
In our previous JVM implementation, we’ve also used the @SneakyThrows annotation from the lombok library to eliminate such boilerplate code and keep our implementation clean.
JVM’s Exception Handling Implementation - The Exception Table
Inside the Code attribute as defined in the JVM specification, there exists a structure called exceptionHandlers. Previously, during parsing, we simply ignored this part and did not process it.
This structure stores information about exception handlers, and now we need to parse it according to the JVM specification.
JVM Instructions Related to Exception Handling
The most important instruction for exception handling is athrow, which throws an exception—an instance (objref) of a class that inherits from java.lang.Throwable.
When an exception is thrown, the JVM first searches the current method’s Code attribute for a suitable handler. If none is found, the exception is propagated to the caller.
For exact implementation details, please carefully read the JVM specification sections on the athrow instruction and exception handling.
Besides exceptions thrown by athrow, don’t forget to complete the exception-triggering scenarios previously mentioned in other instructions (such as NPE, ArithmeticException, ClassCastException, etc.).
BTW: The checked exception mechanism in Java is a compile-time check. However, the JVM we are implementing is a runtime environment for Java bytecode. Therefore, we do not need to handle the checked/unchecked distinction—we only need to implement the behaviors specified by the bytecode.
Implementation Details
Since all Java exceptions must inherit from Throwable, and Throwable calls many other methods (you can inspect its source code using an IDE or view the bytecode via javap), passing the simple test cases from Lab3 only means you’re not too far off.
But when executing methods inside Throwable, you may encounter many unknown issues.
Debugging Tip: Where the JVM specification states must be xxx, use assert statements to validate those conditions. Adopting a fail-fast approach can significantly reduce debugging effort.
Additionally, Throwable involves many native methods. These will cause errors when executed. Until we implement native method calls, we can follow our previous approach with IOUtil—add the corresponding methods to the nativeTable and provide our own stub implementations.
This article is licensed under the CC BY-NC-SA 4.0 license.
Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/njuse-jvm-lab4.1/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/