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