一、虚拟机异常
1、异常产生的原因
Java虚拟机会在以下三种情况抛出异常:
- 字节码指令athrow被执行;
- 虚拟机同步检测到程序有非正常的执行情况,此时虚拟机会紧接着在非正常的操作执行后立刻抛出异常。非正常执行包括:
- 当字节码指令的操作违反了Java的语义,例如访问超出数组界限的索引;
- 在程序加载和链接的时候发送了异常。
- 由于以下原因导致了异步异常:
- 类
Thread
或ThreadGroup的stop方法被调用:线程调用stop方法会影响到其他线程或者特定线程组的全部线程,被影响的其他线程产生的异常就是异步异常,因为这些异常可能发生在程序执行的任何地方。
- Java虚拟机的实现发生了内部错误。请参考下一节。
- 类
2、虚拟机异常
当虚拟机发生内部错误,或者资源受限导致虚拟机实现无法完成其应用的语义的时候,虚拟机实现会抛出VirtualMachineError子类的实例。虚拟机规范中没有定义虚拟机应该何时抛出这些问题,所以VirtualMachineError子类的实例可能在虚拟机运行的任何时间被抛出。
VirtualMachineError的子类包括:
- InternalError:内部错误。实现虚拟机的软件、底层主机的系统软件、或者硬件产生的错误,都可能导致Java虚拟机出现内部错误。InternalError是一个异步异常,可能出现在程序的任何位置。
- OutOfMemoryError:当Java虚拟机耗尽了所有虚拟内存和物理内存,并且自动内存管理系统(即垃圾回收系统)无法回收到足够的内存来分配给新创建的对象时,就会抛出OutOfMemoryError。
- StackOverflowError:当虚拟机耗尽了某个线程的全部栈空间,就会抛出StackOverflowError。典型的错误就是线程调用了无穷无尽的递归操作。
- UnknownError:虚拟机无法确定的异常或者错误。
3、异常处理器
Java虚拟机的每个方法都会分配零到多个异常处理器(exception handlers)。异常处理器描述了它在方法代码中的作用范围、能够处理的异常类型(能处理的异常类型包括指定类型及其子类),并指定处理异常的代码的位置。
当异常发生的时候,如果没有找到相应的异常处理器,方法调用就会异常结束(参考下一节)。
异常处理器存放在异常处理表中。当异常发生的时候,Java虚拟机按照异常处理表中异常处理器的先后顺序,从前到后依次查找可以处理当前异常的异常处理器。Java虚拟机不会干涉异常处理器在异常表中的存放顺序,这个顺序是编译器来安排的。只有在class文件中明确定义了异常处理器的查找顺序,才能保证无论class文件是通过何种途径产生的,在Java虚拟机中都能有一直的行为表现。编译器如何安排异常处理器的存放顺序,详情请参考第二章 try-catch的编译,简单来说就是异常处理器的顺序和代码中catch的顺序是一致的。
4、方法调用异常结束
方法调用异常结束指的是在方法执行的过程中,某些指令导致了Java虚拟机抛出了该方法中无法处理的异常,或者在执行过程中遇到字节指令码athrow显示抛出异常并且该方法内部没有进行捕获。
当方法调用异常完成的时候,当前方法的本地变量表和操作数栈都将被丢弃,它对应的栈帧出栈并恢复到方法调用者的栈帧。未被处理的异常会在方法调用者的栈帧中被重新抛出,并且异常结束的方法调用不会给调用者返回值。如果在调用者的依然无法处理这个异常,将不断重复上面的操作直到调用链的顶部,如果依然无法处理,整个线程都将被终止。
5、指令重排带来的影响
Java虚拟机规范要求,当异常抛出、程序控制器发生转移的那一刻,所有异常抛出位置之前的字节码指令产生的影响都是可见的,之后的字节码指令则不应该产生效果。如果Java虚拟机执行的是指令重排等优化之后的代码,有一些异常出现位置之后的代码已经执行,那么虚拟机必须保证这些提前执行的代码产生的影响对用户程序来说是不可见的。
二、try-catch的编译
1、异常抛出
异常通过throw关键字抛出。throw的编译:
编译前:
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}
}
编译后:
Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to its constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc
2、异常捕获
异常的捕获是通过try-catch实现的。try-catch的编译:
编译前:
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}
编译后:
Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method:
// Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc
可以看到,编译后try语句没有生成任何指令。如果程序执行过程中没有异常抛出,那么程序犹如没有使用try结构。没有try结构的代码编译后:
Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return
3、编译后的catch
catch的编译:
上一节中,catch和catch内部代码块编译后的结果:
5 astore_1 // Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method:
// Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc
其中,handExec方法的指令和正常的方法调用完全相同。另外,每一个catch都会在异常表中增加一个异常处理器。异常表中的每一个异常处理器都代表一个当前方法中的catch语句块中的一个可捕获的异常。
根据上述异常表可知,如果在0~4步操作中间有TestExec异常被抛出,那么操作将转移到第5步操作继续执行。第5步操作就是catch语句块的实现步骤。如果抛出异常的类型部署TestExec,那么不能进行捕获,这个异常将被抛出给当前方法的调用者。
多个catch并列:
一个try可以包含多个catch,例如:
void catchTwo() {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc(e);
} catch (TestExc2 e) {
handleExc(e);
}
}
编译后:
Method void catchTwo()
0 aload_0 // Begin try block
1 invokevirtual #5 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Beginning of handler for TestExc1;
// Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #7 // Invoke handler method:
// Example.handleExc(LTestExc1;)V
11 return // Return after handling TestExc1
12 astore_1 // Beginning of handler for TestExc2;
// Store thrown value in local var 1
13 aload_0 // Push this
14 aload_1 // Push thrown value
15 invokevirtual #7 // Invoke handler method:
// Example.handleExc(LTestExc2;)V
18 return // Return after handling TestExc2
Exception table:
From To Target Type
0 4 5 Class TestExc1
0 4 12 Class TestExc2
可以看到,一个try配置多个catch的情况,在编译好的代码中,多个catch语句块中的代码连续排列,并且异常表中也有相应的连续排列的异常处理器,排列顺序和源码中catch的顺序完全一致。
如果在上述程序执行过程中,try语句块中抛出了一个异常可以被多个catch捕获,那么Java虚拟机将按照从上到下的顺序选择处理这个异常的catch语句块;如果try语句块中抛出了所有catch语句块都不能捕获的异常,那么Java虚拟机将会把异常抛出给当前方法的调用者,而当前方法中所有的catch语句块编译的代码都不会被执行。
catch嵌套:
try-catch语句可以嵌套使用编译后的语句和一个try语句块配置多个catch语句块编译后的代码相似。
void nestedCatch() {
try {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc1(e);
}
} catch (TestExc2 e) {
handleExc2(e);
}
}
编译后的代码:
Method void nestedCatch()
0 aload_0 // Begin try block
1 invokevirtual #8 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Beginning of handler for TestExc1;
// Store thrown value in local var 1
6 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #7 // Invoke handler method:
// Example.handleExc1(LTestExc1;)V
11 return // Return after handling TestExc1
12 astore_1 // Beginning of handler for TestExc2;
// Store thrown value in local var 1
13 aload_0 // Push this
14 aload_1 // Push thrown value
15 invokevirtual #6 // Invoke handler method:
// Example.handleExc2(LTestExc2;)V
18 return // Return after handling TestExc2
Exception table:
From To Target Type
0 4 5 Class TestExc1
0 12 12 Class TestExc2
可以看到,这里的字节指令码和上面一个try配置多个catch是没有区别的,catch语句块的嵌套关系,只是体现在异常表中。异常表中的From和To圈定的范围,可以体现catch语句块之间的嵌套关系。如果里层的try中抛出异常,优先考虑里层的catch语句块能否处理这个异常,如果里层不能处理,才会考虑使用外层的catch语句块去处理这个异常。
catch的处理范围:
这里有个有意思的事情。异常处理器的处理范围,是From和To之间的代码,包括From但是不包括To指示的那一行代码。