JVM如何实现反射

  反射是框架设计的重要手段之一,如Spring的Ioc就是通过反射创建bean实例对象的。虽然反射很常见,但是它的性能损耗也是很昂贵的,甚至是甲骨文关于反射的教学网页,也强调了反射性能开销大的缺点。

 

一. 反射方法的调用

  首先我们来看看反射方法的调用,也就是Method.invoke()。

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是委托给MethodAccessor实现的。MethodAccessor 是一个接口,它有两个已有的具体实现:一个通过本地方法来实现反射调用,另一个则使用了委派模式。为了方便记忆,我便用“本地实现”和“委派实现”来指代这两者。

  每个Method实例第一次调用都会生成一个委托实现,它所委派的具体实现便是一个本地实现。本地实现很好理解,当我们进入虚拟机后,就能拿到class对象或者class实例对象所属方法的内存地址,再通过c++,将传入的参数准备好,直接调用这个方法即可。

  你可能会问,为什么不直接用本地实现,而要通过一个委托实现呢?这是因为除了本地实现,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。

  动态实现的速度比本地实现快20倍,这是因为它不用在java和c++质检来回切换。但是动态实现生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍。到这里想一想,如果是你怎么使用这两种方式呢?相信你已经想到了,设置一个阈值判断。实际行虚拟机也是这么做的,这个值被设置为了15,当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。

 

二. 反射的开销

  方法的反射调用会带来不少性能开销,原因主要有三个:变长参数方法导致的 Object 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。

posted @ 2020-09-15 18:57  Super-Yan  阅读(256)  评论(0编辑  收藏  举报