您的位置:首页 > 其它

07 JVM 是如何实现反射的

伊甸之恋 2019-01-21 21:46 260 查看 https://www.cnblogs.com/yuepen

Java 中的反射

反射是 Java 语言的一个相当重要的特性,它允许正在运行的 Java 程序观测,甚至是修改程序的动态行为。

我们可以通过 Class 对象枚举该类中的所有方法,还可以通过 Method.SetAccessible 让过 Java 语言的访问权限,在私有方法所在类之外的地方调用该方法。

反射在 Java 中的引用十分广泛。日常我们用的 Java 继承开发工具 IDE 便运用了这一功能。比如,敲下点号时,IDE便会根据点号之前的内容动态的展示可以访问的字段或者方法。Java 调试器,能够在调试过程中枚举某一对象所有字段的值。当然,这些功能的实现也用到了语法树。在 Web 开发中,我们接触到的各种通用框架。为了保证框架的可扩展性,旺旺借助 Java 的反射机制,根据配置文件加载不同的类。例如 Spring 框架的以来反转 IOC。

反射调用的实现

首先,看一下 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);
}
}

上面代码中,invoke 方法实际上委派给了 MethodAccessor 来处理。MethodAccessor 是一个接口,有两个具体的实现。一个是通过本地方法来实现反射调用,另一个则使用了委派模式。

每个 Method 实例第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现。在反射过程中,反射调用先是调用了 Method.invoke,然后进入委派实现 DelegatingMethodAccessorlmpl,再然后进入本地实现 NativeMethodAccessorlmpl,最后到达目标方法。

有个疑问,为什么反射调用还要采取委托实现作为中间层,为何不直接交给本地实现?

其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现)来直接使用 invoke 指令来调用目标方法。之所以采用委派实现,是为了能够在本地实现和动态实现中切换。动态实现和本地实现相比,运行效率更高。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,单由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要更快。

实际中,许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15,当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程叫 inflation。

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

反射调用的开销

在反射中 Class.forName,Class.getMethod 以及 Method.invoke 这三个方法,Class.forName 会调用本地方法,Class.getMethod 会遍历该类的共有方法。如果没有匹配,则会遍历父类的方法。在以 getMethod 为代表的查找方法操作中,会返回查找得到结果的一份拷贝。因此,应该避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。实际开发中,我们也往往会缓存 Class.forName,Class.getMethod 的结果,因此,下面只关注反射调用本身的性能开销。

第一,Method.invoke 中的第二个参数是一个可以变长度的 Object 数组,数组中存放的都是对象类型。如果我们存入参数是基本类型,可以提前装箱,减少性能损耗。

第二,可以关闭反射调用的 inflation 机制,从而取消委派实现,并且直接使用动态实现。

第三,每次反射调用都会检查目标方法的权限,而这个检查同样可以在 Java 代码里关闭。

反射 API 简介

使用反射 API 第一步便是获取 Class 对象。获取对象有以下三种方式:
1:使用静态方法 Class.forName 来获取
2:调用对象的 getClass 方法
3:直接使用 类名+“.class” 访问。对于基本类型来说,他们的包装类型拥有一个名为 "TYPE" 的 final 静态字段,指向该基本类型对应的 Class 对象。

例如,Integer.TYPE 指向 int.class。对于数组类型来说,可以使用 类名+[].class 来访问,如 int[].class。

除此之外,Class 类和 java.lang.reflect 包中还提供了许多返回 Class 对象的方法。

获得 Class 对象之后,就可以正式使用反射功能了。下列为常用的几项:
1:使用 newInstance() 生成一个该类的实例。该类必须有一个无参构造函数

2:使用 isInstance(Object) 判断一个对象是否是该类的实例,语法上等同于 instanceof。
3:使用 Array.newInstance(Class,int) 来构造该类型的数组。
4:使用 getFields()/getConstructors()/getMethods() 来访问该类的成员。

问答

Q:当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码...

动态生成发生在第15次(从0开始数的话),所以第15次比较耗时。

Q:什么是 inflation 机制

反射的inflation机制是当反射被频繁调用时,动态生成一个类来做直接调用的机制,可以加速反射调用

总结

本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

标签: