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

本文详细介绍了Java中的异常处理机制,包括Error与Exception的区别、运行时异常和非运行时异常的分类,以及异常指令和异常处理表在代码中的应用。重点讲解了如何通过athrow指令抛出异常和try-catch块处理异常的过程。
摘要由CSDN通过智能技术生成

异常处理是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开始,已经不再使用这些指令,我们不讨论这三条指令。

看一个例子

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 的构造方法:

public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
}

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

private native Throwable fillInStackTrace(int dummy);

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

异常处理表

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

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 的异常处理表,从而可以知道异常处理的优先级。

异常指令实现

    @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 里面。

储存堆栈信息

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

测试

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值