写自己的JVM(0xA)-异常处理

异常处理是Java语言非常重要的一个语法,我们从Java虚拟机的角度来讨论异常是如何被抛出和处理的。

异常结构

Error与Exception

Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。

运行时异常和非运行时异常

运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

异常指令

在代码中抛出和处理异常是由athrow指令和方法的异常处理表配合完成的,我们将重点讨论这一点。

在Java 6之前,Oracle的Java编译器使用jsr、jsr_w和ret指令来实现finally子句。从Java 6开始,已经不再使用这些指令,我们不讨论这三条指令。

看一个例子

typescript 复制代码
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);
}

查看其编译后的字节码:

在调用了异常对象的 new 方法后,就执行了 athrow 指令,与我们上面说的指令对的上。

有一点需要注意,就是异常对象的构造方法会调用到 Throwable 的构造方法:

ini 复制代码
public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
}

这里的 fillInStackTrace 最终会调用到一个 native 方法:

java 复制代码
private native Throwable fillInStackTrace(int dummy);

也就是说,要想抛出异常,Java虚拟机必须实现这个本地方法。

异常处理表

异常处理是通过try-catch句实现的,看一个例子,代码如下:

typescript 复制代码
private static void foo(String[] args) {
    try {
        bar(args);
    } catch (NumberFormatException e) {
        System.out.println(e.getMessage());
    }
}

看字节码:

看起来没有什么特殊的地方,只不过在第 3 行(code偏移4)的字节码处有一个 goto 指令。

从字节码来看,如果没有异常抛出,则会直接goto到return指令,方法正常返回。那么如果有异常抛出,goto和return之间的指令是如何执行的呢?

答案是查找方法的异常处理表。异常处理表是Code属性的一部分,它记录了方法是否有能力处理某种异常。我们看看这个方法的异常处理表:

可以看到,在 pc 的 0-4 范围的指令出了异常的话,就会去匹配 NumberFormatException 类型,如果匹配的上,那么就跳转到 7 位置。如果没有匹配上,那么Java虚拟机会进一步查看它的调用者方法的异常处理表。这个过程会一直继续下去,直到找到某个异常处理项,或者到达Java虚拟机栈的底部。

有兴趣的可以看一下多个 catch 的异常处理表,从而可以知道异常处理的优先级。

异常指令实现

ini 复制代码
    @Override
    public void execute(StackFrame frame) {
        MyObject exceptionObj = frame.getOperandStack().popRef();
        if (exceptionObj == null) {
            throw new MyJvmException("java.lang.NullPointerException");
        }
        MyThread thread = frame.getThread();
        if (!findAndGotoExceptionHandler(thread, exceptionObj)) {
            handleUncaughtException(thread, exceptionObj);
        }
    }

    private void handleUncaughtException(MyThread thread, MyObject exceptionObj) {
        thread.clearStack();
        MyObject value = exceptionObj.getRefFieldValue("detailMessage", "Ljava/lang/String;");
        String string = MyString.toString(value);
        Log.e(string);
        NThrowable.StackTraceElement[] elements = (NThrowable.StackTraceElement[]) exceptionObj.getExtra();
        for (NThrowable.StackTraceElement element : elements) {
            Log.e(element.toString());
        }
    }

    private boolean findAndGotoExceptionHandler(MyThread thread, MyObject exceptionObj) {
        do {
            StackFrame stackFrame = thread.currentStackFrame();
            int pc = stackFrame.getNextPc() - 1;
            MyMethod myMethod = stackFrame.getMyMethod();
            int handlePc = myMethod.findExceptionHandler(exceptionObj.getMyClass(), pc);
            if (handlePc > 0) {
                OperandStack operandStack = stackFrame.getOperandStack();
                operandStack.clear();
                operandStack.pushRef(exceptionObj);
                stackFrame.setNextPc(handlePc);
                return true;
            }
            thread.popStackFrame();
        } while (!thread.isStackFrameEmpty());
        return false;
    }

myMethod.findExceptionHandler 方法是用来判断该方法是否可以处理这个异常。如果可以处理异常,在跳转到异常处理代码之前,要先把操作数栈清空,然后把异常对象引用推入栈顶。

如果遍历完方法栈,都不能处理,那么就打印堆栈信息。堆栈信息是在 native 方法里面实现的,我们的实现代码在 NThrowable 里面。

储存堆栈信息

ini 复制代码
    public static class FillInStackTrace implements NativeMethod {

        @Override
        public void invoke(StackFrame frame) {
            MyObject thisObj = frame.getLocalVariableTable().getRef(0);
            frame.getOperandStack().pushRef(thisObj);
            StackTraceElement[] stackTraceElements = createStackTraceElements(thisObj, frame.getThread());
            thisObj.setExtra(stackTraceElements);
        }

        private StackTraceElement[] createStackTraceElements(MyObject thisObj, MyThread thread) {
            // 栈顶两帧正在执行fillInStackTrace(int)和fillInStackTrace()方法,所以需要跳过这两帧。
            int fillFrame = 2;
            // 这两帧下面的几帧正在执行异常类的构造函数,所以也要跳过,具体要跳过多少帧数则要看异常类的继承层次。
            int skipFrame = distanceToObject(thisObj.getMyClass()) + fillFrame;
            List<StackFrame> stackTraceFrames = thread.getStackTraceFrames(skipFrame);
            StackTraceElement[] stackTraceElements = new StackTraceElement[stackTraceFrames.size()];
            for (int i = 0; i < stackTraceElements.length; i++) {
                stackTraceElements[i] = createStackTraceElement(stackTraceFrames.get(i));
            }
            return stackTraceElements;
        }

        private StackTraceElement createStackTraceElement(StackFrame stackFrame) {
            MyMethod method = stackFrame.getMyMethod();
            MyClass myClass = method.getMyClass();
            int nextPc = stackFrame.getNextPc();

            StackTraceElement stackTraceElement = new StackTraceElement();
            stackTraceElement.className = myClass.getJavaName();
            stackTraceElement.fileName = myClass.getSourceFile();
            stackTraceElement.lineNumber = method.getLineNumber(nextPc - 1);
            stackTraceElement.methodName = method.getName();

            return stackTraceElement;
        }

        private int distanceToObject(MyClass myClass) {
            int distance = 0;
            MyClass superClass = myClass.getSuperClass();
            while (superClass != null) {
                distance++;
                superClass = superClass.getSuperClass();
            }
            return distance;
        }
    }

由于栈顶两帧正在执行 fillInStackTrace(int)fillInStackTrace()方法,所以需要跳过这两帧。这两帧下面的几帧正在执行异常类的构造函数,所以也要跳过,具体要跳过多少帧数则要看异常类的继承层次。distanceToObject()函数计算所需跳过的帧数。

测试

相关推荐
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅15 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊15 小时前
jwt介绍
前端
爱敲代码的小鱼15 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax