JVM的反射机制

  • JVM的反射机制是在Java类编译成字节码的时候,通过jdk提供的工具类的反射方式访问类字节码的字段、方法。并且可以进行一些在Java代码中不能够实现的功能。
  • 反射使用的经典场景:Spring的ioc的依赖注入,以及一些IDE的代码开发联想功能;
  • 之前通过JVM的反射机制还编写了Hibernate的变种版(针对旅游系统的全文检索,对全量索引的数据源功能)。
  • 以上对于JVM的反射功能是通过Java原生代码是实现不了的,但对于反射也是不能频繁使用的,反射机制的效率是很低的,尤其是在统一进程反复调用的时候,反射机制的效率会更加低,以下要通过事列来说明反射机制效率低下的原因。

反射调用的实现
反射的工具代码都在jdk的java.lang.reflect包下
调用代码:

public final class Method extends Executable {
    @CallerSensitive
    public Object invoke(Object var1, Object... var2) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    //省略......
    MethodAccessor var4 = this.methodAccessor;
    if (var4 == null) {
        var4 = this.acquireMethodAccessor();
    }
    return var4.invoke(var1, var2);
}
  • 可以看到method.invoke()方法是委派给了MethodAccessor来处理.MethodAccessor是一个接口,有两个实现:“本地实现”和“委派实现”。只要通过Method.invoke()来调用反射就会生成一个委派实现,只是这个委派实现就是“本地实现”,进入到jvm虚拟久就有方法的地址,该地址指向目标方法,反射只是把参数准备好,然后在调用目标方法。
    package jvm.java7method;
    
    import java.lang.reflect.Method;
    public class MethodTestV0 {
        public static void target(int i) {
            new Exception("#" + i).printStackTrace();
        }
        public static void main(String[] args) throws Exception{
            Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV0");
            Method method = classTestVo.getMethod("target",int.class);
            method.invoke(null,0);
        }
    }

执行结果:
在这里插入图片描述
根据执行结果看到对于Method.invoke()的调用,首先会是“委派实现”:DelegatingMethodAccessorImpl,之后才是“本地实现”:NativeMethodAccessorImpl,最后才是目标方法的调用。之所以采用“委派实现”,可以方便对“动态实现”和“本地实现”进行切换。
“动态实现”:GeneratedMethodAccessor1.class。动态实现的效率要高出本地实现很多倍,但动态实现在编译成字节码时是非常耗时的。本着反射大部分都是调用一次,所以jvm设置了阀值15(是从0开始计算,>15)。当反射的调用次数>15时,jvm将动态生成字节码,“委派实现”中的实现对象将切换到“动态实现”。
在这里插入图片描述
事例代码:

package jvm.java7method;

import java.lang.reflect.Method;
public class MethodTestV1 {
    public static void target(int i) {
        new Exception("#" + i).printStackTrace();
    }
    public static void main(String[] args) throws Exception{
        Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV1");
        Method method = classTestVo.getMethod("target",int.class);
        for (int i = 0;i < 20;i++) {
            method.invoke(null,0);
        }
    }
}

在这里插入图片描述
这样看不是太明显,可以改变输出参数,打印加载的类:java -verbose:class xx.xx.MethodTestV1
在这里插入图片描述
可以看到,在执行到15次时虚拟机出发了动态实现,在16次调用则有委派实现的实现对象切换到“动态实现”。

反射性能的开销
反射的主要操作:Class.forName()和Class.getMethod()调用本地实现,其中,getMethod()则会遍历类中的共有方法,类中没有则遍历父类,所以是非常耗时的,尤其在一些热点代码中可以采用本地缓存的方式来缓存结果。
测试代码:

  • 基准测试代码:
package jvm.java7method;
import java.lang.reflect.Method;
public class MethodTestV2 {
   public static void target(int i) {}
   public static void main(String[] args) throws Exception{
       Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV2");
       Method method = classTestVo.getMethod("target",int.class);
       long current = System.currentTimeMillis();    //获取当前时间
       for (int i = 0;i < 2000000000;i++) {
           if (i % 100000000 == 0) {                 //输出1亿次的执行时间
               long temp = System.currentTimeMillis();
               System.out.println(temp - current);
               current = temp;
           }
           method.invoke(null,128);
       }
   }
}

以上基准代码获取基准时间:???

  • 自动装箱测试代码:
package jvm.java7method;
import java.lang.reflect.Method;
public class MethodTestV2 {
    public static void target(int i) {
    }
    public static void main(String[] args) throws Exception{
        Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV2");
        Method method = classTestVo.getMethod("target",int.class);
        long current = System.currentTimeMillis();    //获取当前时间
        Integer autoI = new Integer(128);       //手动装箱
        for (int i = 0;i < 2000000000;i++) {
            if (i % 100000000 == 0) {                 //输出1亿次的执行时间
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null,autoI);
        }
    }
}

运行结果手动装箱执行速度稍快,原因是jvm会缓存装箱数据,否则会新建,这样会消耗时间。
数组参数的传入:

package jvm.java7method;
import java.lang.reflect.Method;
public class MethodTestV2 {
    public static void target(int i) {
    }
    public static void main(String[] args) throws Exception{
        Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV2");
        Method method = classTestVo.getMethod("target",int.class);
        long current = System.currentTimeMillis();    //获取当前时间
        Object[] arg = new Object[1];
        arg[0] = 128;
        for (int i = 0;i < 2000000000;i++) {
            if (i % 100000000 == 0) {                 //输出1亿次的执行时间
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null,arg);
        }
    }
}

执行结果会比手动装箱更慢,原因:
手动装箱不会进行gc操作,invoke的数组参数会被认为是不逃逸对象,不逃逸对象会分配栈或是虚拟分配,不占用堆内存。而传入数组参数,不能进行不逃逸优化,所以执行效率会更慢。后续会有对逃逸有更详细分析,代码和字节码的分析结果会更新。
取消委派本地实现,直接调用动态实现

package jvm.java7method;
import java.lang.reflect.Method;
public class MethodTestV4 {
    public static void target(int i) {
    }
    public static void main(String[] args) throws Exception{
        Class<?> classTestVo = Class.forName("jvm.java7method.MethodTestV4");
        Method method = classTestVo.getMethod("target",int.class);
        method.setAccessible(true);
        long current = System.currentTimeMillis();    //获取当前时间
        for (int i = 0;i < 2000000000;i++) {
            if (i % 100000000 == 0) {                 //输出1亿次的执行时间
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null,128);
        }
    }
}

在运行前要添加执行参数:
-Djava.lang.Integer.IntegerCache.high=128 -Dsun.reflect.noInflation=true
运行速度明显快于其他,关于动态实现的快原因在后续继续给出???
综上述:
反射的性能开销主要体现在这几个方面 :
(1)数组参数的传入
(2)自动装箱和手动装箱的区别
(3)逃逸对象的优化
(4)动态实现的内联调用
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/nmtcttn/article/details/83472069