JVM如何处理异常
异常处理的两大组成要素是捕获异常和抛出异常。
抛出异常分为显式和隐式两种,显式异常的主体是应用程序,显式异常是在程序中通过throw关键字手动抛出异常实例。隐式抛异常主体是JVM,他指的是JVM运行过程中碰到无法继续运行的异常,自动抛出异常,比如数组越界异常。
异常捕获相关的代码块:
try: 标记需要被进行异常监控的代码。
catch: 捕获在try中触发的指定类型的异常。
finally: 用来声明一段必定运行的代码。设计初衷是为了避免跳过某些关键的清理代码,比如关系一打开的系统资源。
异常的基本概念:
所有异常都是throwable的子类。throwable下的两大子类分别是exception和error。
exception:程序可能需要捕获并且处理的异常,他有一个特殊的子类runException,表示程序虽然无法继续执行了,但还可以补救,例如数组越界等异常。
error:程序不应捕获的异常,当程序触发error时,他的执行状态已经无法恢复了,需要中断线程或终止虚拟机。
构造异常实例比较消耗资源。构造异常实例时,JVM需要生成该异常的栈轨迹,该操作会逐一访问当前线程的JAVA栈帧,并且记录下各种调试信息,包括栈帧指向的方法的名字,方法的类名,文件名,以及代码的哪一行触发了该异常。
JVM是如何捕获异常的:
编译生成的字节码中,每个方法都附带一个异常表,异常表的每一个条目代表一个异常处理器,并且包含from指针,to指针,target指针,以及所捕获的异常类型。这些指针的值都是字节码索引,用来定位字节码。from指针和to指针标识了异常处理器的监控范围,比如try代码块覆盖的区域,target指针指向的是异常处理器的其实位置,比如catch代码块的起始位置。
public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}
// 对应的Java字节码
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception // 异常表条目
如上代码,该方法包含一个异常条目。from指针是0,to指针是3,表示异常条目的监控范围从0开始,到索引值为3时结束。target是6,表示异常处理器从索引6的字节码开始。
当程序触发异常时,JVM会从上到下遍历异常表中的所有条目,当触发异常的字节码索引值在某个异常表条目的监控范围内,JVM会判断抛出的异常和该条目想捕获的异常是否匹配。匹配的话JVM会将控制流转移到该条目的target指针指向的字节码。
finally代码块编译结果是:复制finally代码块里的内容,分别放在代码块所有正常执行路径以及异常执行路径的出口中。
try-with-resource:
java7之前关系资源都用finally代码块,来确保资源在正常或者异常状态下都可以关闭。资源的关闭容易触发异常,如果同时打开多个资源,那每个资源都要对应一个finally,代码会变得非常繁琐。
java7后的try-with-resource语法糖,极大地变化了代码,try代码块后声明并实现了AutoCloseable接口的类,编译器将自动添加close操作。try-with-resource还会使用suppressed异常的功能,来避免原异常消息。
public class Foo implements AutoCloseable {
private final String name;
public Foo(String name) { this.name = name; }
@Override
public void close() {
throw new RuntimeException(name);
}
public static void main(String[] args) {
try (Foo foo0 = new Foo("Foo0"); // try-with-resources
Foo foo1 = new Foo("Foo1");
Foo foo2 = new Foo("Foo2")) {
throw new RuntimeException("Initial");
}
}
}
// 运行结果:
Exception in thread "main" java.lang.RuntimeException: Initial
at Foo.main(Foo.java:18)
Suppressed: java.lang.RuntimeException: Foo2
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo1
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo0
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
JVM如何实现反射:
spring框架中,为了保证可扩展性,框架借助了JAVA的反射机制,根据配置文件来加载不同的类,比如sping的控制反转IOC。
查阅method.invoke的源代码,实际上是委派给methodAccessor来处理。methodAccessor是一个借口,他有两个实现,一个是通过本地方法来实现反射调用,另一个是委派模式。每个method实例第一次反射调用都会生成一个委派实现,委派的具体实现就是一个本地实现。本地实现:当进入JVM内部后,我们便拥有一个method实例所指向方法的具体地址。然后就是将传入的参数准备好,然后调用进入目标方法。
java的反射调用机制还有另一种动态生成字节码的实现,直接使用invoke指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现和动态实现之间切换。
考虑到许多反射调用仅会执行一次,所以JVM设置了一个阈值15,当某个反射调用次数小于15,采用本地实现,大于15,便开始动态生成字节码,并将委派实现的委派对象切换成动态实现。
反射调用的开销:
class.getmethod会遍历该类的所有方法,如果没有匹配到,则会遍历父类的共有方法。这两个操作很消耗性能。 应当避免在热点代码区域返回method操作和getDecaredMethod方法,以减少不必要的堆内存消耗。实践中应用程序会缓存forName和getMethod的结果。
method.invoke方法是一个变长参数的方法,在字节码层面他的最后一个参数是object数组,java编译器会在方法调用处生成一个长度为传入参数数量的object数组,并将参数一一存入该数组中。 由于object数组不能存储基本类型,java编译器对传入的基本类型参数进行自动拆箱。