Java虚拟机学习07 | JVM是如何实现反射的

https://time.geekbang.org/column/article/12192

反射的应用情景

  • 在日常开发中使用Java编译器(IDE)的时候,当敲入点号时,IDE会根据点号之前的内容动态展示可以访问的字段与方法(在文章后续的评论中有人指出IDE在实现这个功能大部分是使用语法树来实现的)
  • 在开发过程中使用的框架,例如Spring的IOC便是依赖反射机制

反射调用的实现

public final class Method extends Executable {
  ...
  public Object invoke(Object obj, Object... args) throws ... {
    ... // 权限检查
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
      ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
  }
}

当查看Method.invoke源码时可以发现它的具体实现时委派给MethodAccessor来处理的,MethodAccessor是一个接口它有三个具体实现类:

  • NativeMethodAccessorImpl:通过本地实现来实现反射调用

  • DelegatingMethodAccessorImpl:应用了委派模式,反射具体实现都先通过这个委派然后进行下一步调用,例如调用本地实现

  • MethodAccessorImpl:实际上只是这个类实现了MethodAccessor接口,上面两个类都是继承这个类

委派模式的原理为类B和类A是两个互相没有任何关系的类,B具有和A一模一样的方法和属性,并且调用B中的方法、属性就是调用A中同名的方法和属性。B好像就是一个受A授权委托的中介。第三方的代码不需要知道A的存在,也不需要和A发生直接的联系,通过B就可以直接使用A的功能,这样既能够使用到A的各种功能,又能够很好的将A保护起来,一举两得。

  • 每个Method实例的第一次调用都会生成一个委派实现,它所委派的是一个本地实现. 当进入Java虚拟机内部时,我们拥有了Method实例所指向的方法的具体地址,只要将准备好的参数传入.

      具体调用栈:反射调用---->Method.invoke---->委派实现(DelegatingMethodAccessorImpl)---->本地实现(NativeMethodAccessorImpl)---->目标方法

  • 当反射调用超过15次时,Java会使用动态生成字节码的实现方式直接使用invoke指令调用目标方法. 这种方式比本地实现要快20倍,因为动态实现无需经过Java到C++再到Java的切换,但是因为生成字节码的消耗,仅仅一次调用的话反而是本地实现要快3倍左右

上面15这个是Java设置的阀值,可以通过-Dsun.reflect.inflationThreshold= 来调整.

反射调用的 Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的。这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现。

Inflation 指将委派时间的委派对象切换到动态实现的过程

反射调用的开销

在反射调用之前我们进行Class.forName,Class.getMethod,然后才是Method.invoke. 其中Class.forName调用了本地方法,Class.getMethod会遍历这个类的所有公有方法,如果没找到目标会查找父类的所有公有方法,这两个方法将十分耗时;同时在以getMethod为代表的查找方法会返回一份结果的拷贝,因此在热点方法中应该避免这类方法,减少堆空间消耗

接下来是反射自身的性能开销的影响因素:

  • 当反射调用位于热点代码中,同样会触发即时编译,即时编译会将反射目标方法的调用内联进来,从而消除反射的消耗
  • Method.invoke是一个可变长参数方法,在字节码中,它的最后一个参数是Object参数,Java编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并且将传入参数一一存入这个Object数组中
  • 因为Object数组不能存基本类型,所以Java编译器会对基本数据类型自动装箱

猜你喜欢

转载自blog.csdn.net/qq_34332035/article/details/87807386