JVM是如何实现反射的

阅读: 评论:0

JVM是如何实现反射的

JVM是如何实现反射的

在我们聊起JVM是如何实现反射的之前,我们先来说一下什么是反射。

反射:反射就是在运行过程中获取类的信息,并能调用类的方法。

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

举例来说,我们可以通过class对象枚举该类中的所有方法,我们还可以通过java的反射包里的Method.setAccessible绕过java语言的访问权限,在私有方法类以外的地方调用里面的方法。

说一个反射在我们开发中很常见的情况吧,在我们使用IntelliJ Idea进行开发的时候,我们使用.的时候,会自动告诉我们可以调用什么方法,这就是开发中一种十分常见的反射的效果。

在我们使用Web开发中,SpringIOC的依赖反转就是依赖于Java的反射机制,但我们也同时可以感受到,反射是一种很浪费性能的事情,在Oracle官方也特意提到了其对于性能消耗过大的事情。

接下来就来说一下反射的实现机制,以及性能糟糕的原因。

反射调用的实现

首先我们来看看方法的反射调用是如何实现的,就是Method.invoke

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

我们可以通过上面的方法发现,它实际上将反射的业务委派给了MethodAccessor来实现,MethodAccessor是一个接口,它有两个具体的接口实现:一个通过本地方法来实现反射调用,另一个则使用了委派模式。为了方便记忆,我便用了“本地使用”和“委派实现”来指代这两者。

每个Method实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现。本地实现非常容易理解。当进入了JVM内部之后,我们便拥有了Method实例指向具体的方法地址。这时候,反射调用就会将参数都准备好,自动填充到对应的方法内。

那我们使用委派实现的具体作用是什么呢,直接交给本地不好吗?

其实,Java的反射机制还设立了另一种动态生成字节码的实现,简称动态实现,并且委派实现的意义就是在于,可以在本地实现和动态实现中切换。

在这里我说一下,动态实现的总体速度是比本地实现快上几十倍的,但是问题在于,生成字节码然后解码的过程倒是很浪费资源,所以,如果你就调用一次方法去反射,得不偿失啊。

所以JVM就规定了反射次数的一个规范:当调用invoke方法<15次,就本地实现,当≥15次,就用动态生成字节码的方法反射。

举一个反射调用20次代码实现

// v1 版本
import flect.Method;public class Test {public static void target(int i) {new Exception("#" + i).printStackTrace();}public static void main(String[] args) throws Exception {Class<?> klass = Class.forName("Test");Method method = Method("target", int.class);for (int i = 0; i < 20; i++) {method.invoke(null, i);}}
}# 使用 -verbose:class 打印加载的类
$ java -verbose:class Test
...
java.lang.Exception: #14at Test.target(Test.java:5)at java.base/flect.NativeMethodAccessorImpl .invoke0(Native Method)at java.base/flect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)at java.base/flect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)at java.base/flect.Method.invoke(Method.java:564)at Test.main(Test.java:12)
[0.158s][info][class,load] ...
...
[0.160s][info][class,load] flect.GeneratedMethodAccessor1 source: __JVM_DefineClass__
java.lang.Exception: #15at Test.target(Test.java:5)at java.base/flect.NativeMethodAccessorImpl .invoke0(Native Method)at java.base/flect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)at java.base/flect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)at java.base/flect.Method.invoke(Method.java:564)at Test.main(Test.java:12)
java.lang.Exception: #16at Test.target(Test.java:5)at flect.GeneratedMethodAccessor1 .invoke(Unknown Source)at java.base/flect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)at java.base/flect.Method.invoke(Method.java:564)at Test.main(Test.java:12)
...

我们可以从上面日志中看出,从第15次反射调用invoke的时候,虚拟机加载了额外的类,这就是动态实现的证明,并且可以看出,在后面的实现,都是依赖于动态实现去进行反射调用。

这种规范,被我们定义为Inflation

反射调用的开销

下面,我们就来说一下反射带给我们额外性能的开销。我们在刚才的例子中,使用到了

Class.forName():调用本地方法
Method():遍历该类的共有方法。如果没有匹配到,还会遍历父类的共有方法

我们可以看出上面的两个方法实现都是很耗时的,尤其是getMethod(),我们至少要避免getMethod方法在热点代码中少用。

以下贴出一个例子,会将反射执行二十亿次

// v2 版本
mport flect.Method;public class Test {public static void target(int i) {// 空方法}public static void main(String[] args) throws Exception {Class<?> klass = Class.forName("Test");Method method = Method("target", int.class);long current = System.currentTimeMillis();for (int i = 1; i <= 2_000_000_000; i++) {if (i % 100_000_000 == 0) {long temp = System.currentTimeMillis();System.out.println(temp - current);current = temp;}method.invoke(null, 128);}}
}59: aload_2                         // 加载 Method 对象60: aconst_null                     // 反射调用的第一个参数 null61: iconst_162: anewarray Object                // 生成一个长度为 1 的 Object 数组65: dup66: iconst_067: sipush 12870: invokestatic Integer.valueOf    // 将 128 自动装箱成 Integer73: aastore                         // 存入 Object 数组中74: invokevirtual Method.invoke     // 反射调用

并且,进行反射调用的过程中,它内部还会进行其他两个操作。

  1. 由于Method.invoke是一个变长参数方法,在字节码层面它的最后一个参数是Object数组。Java编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并将传入参数一一存储进该数组中。

  2. 由于Object数组不能存储基本类型,Java编译器会对传入的基本类型参数进行自动装箱。

这两个操作除了带来性能开销外,还可能占用堆内存,使GC更加频繁。

那么,如何消除掉这部分开销呢?

关于第二个自动装箱,Java缓存了[-128,127]中所有整数所对应的Integer对象,当需要自动装箱的整数在这个范围内的时候就会自动返回缓存的Integer,否则就需要新建一个Integer对象。

所以为了能不新建Integer对象,可以将缓存的范围也扩大。

本文发布于:2024-02-02 22:05:16,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170688271446743.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:反射   如何实现   JVM
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23