【JVM】深入了解java反射机制

(注:文章总结自极客时间郑雨迪老师的《深入拆解java虚拟机》课程,且自己做了一些测试和修正)

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

1 常用反射API简介

1.1 获取class对象

  1. Class.forName(“类名”)
  2. 对象.getClass() 方法
  3. 类名.class
  4. 对于基本类型来说,它们的包装类型拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。例如,Integer.TYPE 指向 int.class

1.2 类对象的的常用api

  1. 类对象. newInstance(),生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
  2. 类对象.isInstance(Object),判断一个对象是否该类的实例。
  3. Array.newInstance(Class,int),构造该类型的数组。
  4. getFields()/getConstructors()/getMethods(),访问该类的成员。

注:类对象的方法中,方法名中带 Declared 的不会返回父类的成员,但是会返回私有成员;而不带 Declared 的则相反。

1.3 类成员的常用api

  1. Constructor/Field/Method.setAccessible(true) ,绕开 Java 语言的访问限制。
  2. Constructor.newInstance(Object[]),生成该类的实例。
  3. Field.get/set(Object),访问字段的值。
  4. Method.invoke(Object, Object[]),调用方法。

2 反射调用的实现

每个 Method 实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现。本地实现非常容易理解。当进入了 Java 虚拟机内部之后,我们便拥有了 Method 实例所指向方法的具体地址。这时候,反射调用无非就是将传入的参数准备好,然后调用进入目标方法。

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {
        new Exception("#" + i).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", int.class);
        method.invoke(null, 0);
    }
}

运行结果

java.lang.Exception: #0
	at Main.test(Main.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at Main.main(Main.java:14)

可以看到,反射调用先是调用了 Method.invoke,然后进入委派实现(DelegatingMethodAccessorImpl),再然后进入本地实现(NativeMethodAccessorImpl),最后到达目标方法。

为什么要先进入委派实现才调用本地实现呢?

java 反射机制中有另一种动态生成字节码的实现,直接使用invoke指令来调用目标方法。之所以采用委派实现,是为了能够在本地实现和动态生成自己吗实现中切换。

动态生成字节码实现比起本地实现来说,运行效率快20倍左右,这是因为动态实现无需经过Java到C++再到Java的转换,但是因为生成字节码的过程很好使,所以仅调用一次的时候,反而是本地实现更快,快3-4倍左右。

这时候,java虚拟机的机制介入了,java虚拟机设置了一个阈值可以通过 -Dsun.reflect.inflationThreshold= 来调整),当某个反射调用的调用次数在15次以下的时候,只采用本地实现,当达到15次时,开始动态生成字节码,并将委派实现和委派对象切换至动态实现,这个过程我们成为Inflation。

我们来做个测试看下堆栈的不同

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {
        new Exception("#" + i).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", int.class);
        for (int i = 1; i <= 20; i++) {
            method.invoke(null, i);
        }
    }
}

使用java -verbose:class Main.java运行结果

java.lang.Exception: #15
        at Main.test(Main.java:9)
        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:566)
        at Main.main(Main.java:16)
        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:566)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:404)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:179)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:119)
[0.392s][info][class,load] jdk.internal.reflect.ClassFileConstants source: jrt:/java.base
[0.392s][info][class,load] jdk.internal.reflect.AccessorGenerator source: jrt:/java.base
[0.392s][info][class,load] jdk.internal.reflect.MethodAccessorGenerator source: jrt:/java.base
[0.392s][info][class,load] jdk.internal.reflect.ByteVectorFactory source: jrt:/java.base
[0.392s][info][class,load] jdk.internal.reflect.ByteVector source: jrt:/java.base
[0.392s][info][class,load] jdk.internal.reflect.ByteVectorImpl source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.ClassFileAssembler source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.UTF8 source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.Label source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.Label$PatchInfo source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.MethodAccessorGenerator$1 source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.ClassDefiner source: jrt:/java.base
[0.393s][info][class,load] jdk.internal.reflect.ClassDefiner$1 source: jrt:/java.base
[0.394s][info][class,load] jdk.internal.reflect.GeneratedMethodAccessor1 source: __JVM_DefineClass__
java.lang.Exception: #16
        at Main.test(Main.java:9)
        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:566)
        at Main.main(Main.java:16)
        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:566)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:404)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:179)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:119)
java.lang.Exception: #17
        at Main.test(Main.java:9)
        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:566)
        at Main.main(Main.java:16)
        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:566)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:404)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:179)
        at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:119)

从结果可以看出,在第16次开始动态生成字节码,且jvm额外加载了很多类,但第16次调用过程还是本地调用,从第17次开始切换到动态实现。
Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的。这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现。

3 反射调用开销

首先,我们对一个空方法,使用反射调用的方式进行循环20亿次测试耗时,他会在每跑1亿数据打印一次耗时,这里采取将最后五个耗时取平均值的方式作为预热后的峰值性能。

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {

    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", 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, 0);
        }

    }
}

打印结果:

160
231
188
187
191
186
194
188
193
188
184
186
211
184
185
188
184
183
183
184

最后五次取平均值约为184ms,将此值作为基准值

因为方法接受的是个int类型的参数,这时候如果我将invoke方法的参数传为128,以上边同样的方式执行耗时为 383ms,约为基准值的2倍,为什么呢?

我们看下字节码

63: aload_2				// 加载Method对象
64: aconst_null
65: iconst_1
66: anewarray     #13	//Object   // 生成一个数组
69: dup
70: iconst_0
71: sipush        128	
74: invokestatic  #14	//Integer.valueOf	// 将128自动装箱为Integer           
77: aastore				// 放入生成的数组中      
78: invokevirtual #15   // 反射调用         
  1. 由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(感兴趣的同学私下可以用 javap 查看)。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
  2. 由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。

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

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

因此,我们可以将这个缓存的范围扩大至覆盖 128(对应参数-Djava.lang.Integer.IntegerCache.high=128),便可以避免需要新建 Integer 对象的场景。

或者,我们可以在循环外缓存 128 自动装箱得到的 Integer 对象,并且直接传入反射调用中。这两种方法测得的结果差不多,耗时约为196毫秒,为基准的 1.06倍。

然后我们反过来看低一点,每次循环都会因为变长参数生成Object数组,既然我们这段程序中每次反射调用的参数个数是固定的,那个我们可以在循环外去建立一个Object数组,设置好参数,并交给反射调用。

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {

    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", int.class);

        Object[] arg = new Object[1];
        arg[0] = 128;
        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, arg);
        }

    }
}

测试结果耗时约为247ms,约为集中的1.34倍,比之前更糟糕了,为什么呢?

这是因为如果在循环外新建数组,即时编译器无法确定这个数组会不会中途被更改,因此无法优化掉访问数组的操作,可谓是得不偿失。

我们再尝试去优化上一段代码:

  1. 关闭Inflation机制
  2. 关闭每次调用方法的检查权限

我们得到以下代码

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {

    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", int.class);
        method.setAccessible(true);

        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);
        }

    }
}

运行结果耗时为 154ms,为基准的0.83

至此基本榨干了反射调用的水分。

在这个例子中,之所以反射调用能够变得这么快,主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。

在生产环境中,我们往往拥有多个不同的反射调用,对应多个 GeneratedMethodAccessor,也就是动态实现。

由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。

接下来我们修改一下代码污染一下Method.invoke方法的类型profile

import java.lang.reflect.Method;

public class Main {
    public static void test(int i) {

    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Main");
        Method method = clazz.getMethod("test", int.class);
        method.setAccessible(true);
        polluteProfile();

        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);
        }

    }

    public static void polluteProfile() throws Exception {
        Method method1 = Main.class.getMethod("pollute1", int.class);
        Method method2 = Main.class.getMethod("pollute2", int.class);
        for (int i = 0; i < 2000; i++) {
            method1.invoke(null, 0);
            method2.invoke(null, 0); }
    }
    public static void pollute1(int i) { }
    public static void pollute2(int i) { }

}

我们在之前循环之前调用下polluteProfile方法,该方法中循环反射调用另外两个方法,此程序再用之前的耗时计算方式计算为425ms, 是基准的2.3倍。

之所以这么慢,除了没有内联之外,另外一个原因是逃逸分析不再起效。这时候,我们便可以采用刚才 在循环外构造参数数组,并直接传递给反射调用。这样子测得的结果约为基准的 1.7 倍。

这里JVM受到-XX:TypeProfileWidth(类型轮廓)限制能够记录的类型数目,默认值为 2,用户无法设置,所以我们就需要尽量在程序结构避免出现这种情况。

  • 27
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值