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, Object... 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 java.lang.reflect.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 = klass.getMethod("target", int.class);
    for (int i = 0; i < 20; i++) {
      method.invoke(null, i);
    }
  }
}

# 使用 -verbose:class 打印加载的类
$ java -verbose:class Test
...
java.lang.Exception: #14
        at Test.target(Test.java:5)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at Test.main(Test.java:12)
[0.158s][info][class,load] ...
...
[0.160s][info][class,load] jdk.internal.reflect.GeneratedMethodAccessor1 source: __JVM_DefineClass__
java.lang.Exception: #15
       at Test.target(Test.java:5)
       at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
       at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)
       at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
       at java.base/java.lang.reflect.Method.invoke(Method.java:564)
       at Test.main(Test.java:12)
java.lang.Exception: #16
       at Test.target(Test.java:5)
       at jdk.internal.reflect.GeneratedMethodAccessor1 .invoke(Unknown Source)
       at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
       at java.base/java.lang.reflect.Method.invoke(Method.java:564)
       at Test.main(Test.java:12)
...

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

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

反射调用的开销

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

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

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

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

// v2 版本
mport java.lang.reflect.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 = klass.getMethod("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                     // 反射调用的第一个参数 null
   61: iconst_1
   62: anewarray Object                // 生成一个长度为 1 的 Object 数组
   65: dup
   66: iconst_0
   67: sipush 128
   70: invokestatic Integer.valueOf    // 将 128 自动装箱成 Integer
   73: aastore                         // 存入 Object 数组中
   74: invokevirtual Method.invoke     // 反射调用

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

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值