JAVA虚拟机基本原理(三:JVM如何处理异常和如何实现反射)

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编译器对传入的基本类型参数进行自动拆箱。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值