写自己的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()函数计算所需跳过的帧数。

测试

相关推荐
To_OC8 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic8 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志9 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble10 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯11 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司12 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g12 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森12 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http