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

测试

相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css