JVM角度看方法调用-反射篇


JVM角度看方法调用系列文章:

1.JVM角度看方法调用-开篇

2.JVM角度看方法调用-反射篇

3.JVM角度看方法调用-MethodHandle篇

4.JVM角度看方法调用-性能压测篇(码字…)

5.JVM角度看方法调用-Lambda篇(码字…)

​ 在我们平时开发中常用的方法调用有三种:直接调用、反射调用、MethodHandle调用,这一系列文章就围绕着三种调用方式进行原理剖析和性能分析,本文JDK的版本是1.8。


​ 反射是 Java 基础最重要的特性之一,反射可以动态获取信息以及动态调用对象的方法。但由于反射的性能比较糟糕,更多的是用在框架或者服务的初始化阶段,一个高可用服务,其频繁调用的热点代码是不推荐使用反射机制的。接下来这篇文章主要介绍下反射的原理和其糟糕性能的原因以及怎么如何能对反射调用进行一个优化。

一、反射调用的原理

​ 下面这个例子是典型的利用反射进行方法调用,其中反射主要包括两部分:

  1. Class.getMethod 获取Method对象

  2. method.invoke 反射调用目标方法

public class Animal {
  	/**
     * 小动物可以进行简单的加法运算
     * @param one
     * @param two
     * @return
     */
    public int calculation(int one,int two){
        return one+two;
    }
}


public class ReflectCallDemo {
    public static void main(String[] args)throws Exception{
       test(new Animal());
    }
    
    public static void test(Animal animal)throws Exception{
        Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
        Object result = eatMethod.invoke(animal, 1, 2);
        System.out.println("1+2="+result);
    }
}

​ 其中1.Class.getMethod()获取Method对象,在Class实例第一次获取Method时会缓存这个类型的全部Method方法数组,但是这里采用的是SoftReference<ReflectionData> reflectionData是一个软引用,在系统资源不足时还是会回收掉;而且getMethod(String name, Class<?>… parameterTypes)方法需要在缓存的Method[]中再次根据方法签名进行筛选得到需要的Method。所以我们开发中推荐在程序中显式的缓存Class.getMethod()获取到的Method对象,以避免重复获取的开销。

​ 由此可见反射的性能开销就只剩下2.method.invoke() 反射调用目标方法了,这也是本篇的重点,分析其源码,找到开销点,并设法优化。下面先进行源码分析。

Method.invoke()源码如下:

  public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        // 1.方法修饰符权限检查,如:private方法是不允许在本类外部调用的。可以通过 method.setAccessible(true);取消权限检查。
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
      	// 2. 获取MethodAccessor
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        // 3.执行MethodAccessor.invoke
        return ma.invoke(obj, args);
    }

由上述代码可知Method.invoke()方法中真正执行反射调用的是MethodAccessor。

1.1获取MethodAccessor

​ 在Method中有两个关键属性methodAccessor和root。

public final class Method extends Executable {
  ...
	private volatile MethodAccessor methodAccessor;
	private Method              root;
  ...
}

​ 在Class.getMethod时会调用本地native方法获取对应的Method实例命名为root,但是这个对象并不会将引用直接暴露给调用方,而是会copy一个新的Method返回给调用方

Method copy() {
        if (this.root != null)
            throw new IllegalArgumentException("Can not copy a non-root Method");

        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        // copyMethod中的root属性就是native返回的Method实例
  			res.root = this;
  			// copyMethod中的methodAccessor就是root中的methodAccessor属性。
        res.methodAccessor = methodAccessor;
        return res;
    }

由上述代码可知同一个root的全部 Method 拷贝都会使用同一份 methodAccessor,methodAccessor在root的第一个Method拷贝执行invoke方法时被赋值。

private MethodAccessor acquireMethodAccessor() {
       
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
          	// 当root中的methodAccessor为空时即root的第一个Method拷贝执行invoke方法时创建一个新的MethodAccessor
            tmp = reflectionFactory.newMethodAccessor(this);
          	// 赋值给root.methodAccessor.
            setMethodAccessor(tmp);
        }

        return tmp;
    }

接下来看下ReflectionFactory.newMethodAccessor()方法如何创建一个MethodAccessor实现,首先MethodAccessor有DelegatingMethodAccessorImpl、MethodAccessorImpl、NativeMethodAccessorImpl

  • NativeMethodAccessorImpl 是native版本的MethodAccessor实现最终调用native方法invoke0()实现方法调用
  • MethodAccessorImpl:通过ASM技术动态生成一个MethodAccessorImpl的子类并实现invoke方法,这就是java版本的调用
  • DelegatingMethodAccessorImpl 是一个委派模式的MethodAccessor实现,内部包含一个private MethodAccessorImpl delegate属性,执行invoke方法时委派调用delegate.invoke(),他存在的意义是方便在执行了一定次数后由native版本调用切换到java版本调用。
 public MethodAccessor newMethodAccessor(Method var1) {
   		// 是否初始化,未初始化则初始化 
        checkInitted();
        if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
          // 如果设置了 -Dsun.reflect.noInflation=true则直接利用ASM技术创建一个MethodAccessorImpl的子类作为java版本的MethodAccessor返回
          return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
        } else {
         // 默认-Dsun.reflect.noInflation=false,返回DelegatingMethodAccessorImpl委派模式的MethodAccessor实现,设置其delegate属性为NativeMethodAccessorImpl(native版本的MethodAccessor)
            NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
            DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
            var2.setParent(var3);
            return var3;
        }
    }






 private static void checkInitted() {
        if (!initted) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.out == null) {
                        return null;
                    } else {
                      // -Dsun.reflect.noInflation的默认值是false,如果设置为true则一开始直接使用java版本的MethodAccessor实现,此时首次调用会比较耗时,因为ASM动态创建并加载MethodAccessor实现类比较耗时。              
                        String var1 = System.getProperty("sun.reflect.noInflation");
                        if (var1 != null && var1.equals("true")) {
                            ReflectionFactory.noInflation = true;
                        }
												// -Dsun.reflect.inflationThreshold的默认值是15,代表一个Method.invoke的native版本执行15次以后切换成java版本。
                        var1 = System.getProperty("sun.reflect.inflationThreshold");
                        if (var1 != null) {
                            try {
                                ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
                            } catch (NumberFormatException var3) {
                                throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
                            }
                        }

                        ReflectionFactory.initted = true;
                        return null;
                    }
                }
            });
        }
    }

1.2 native版本MethodAccessor的实现

首先看下native版本下返回的DelegatingMethodAccessorImpl执行invoke时的实现

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        return this.delegate.invoke(var1, var2);
}

DelegatingMethodAccessorImpl的invoke实现是委派NativeMethodAccessorImpl调用invoke方法

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
  // 如果native版本执行超过 -Dsun.reflect.inflationThreshold的的值,默认值是15,则设置DelegatingMethodAccessorImpl的delegate属性为java版本的MethodAccessor实现,即切换为java版本
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
          // 切换java版本
            this.parent.setDelegate(var3);
        }
				// native方法
        return invoke0(this.method, var1, var2);
    }

​ 上述代码可知method.invoke反射方法调用,针对同一个root的Method拷贝对象加起来只要执行sun.reflect.inflationThreshold次调用就会切换成java版本,java版本的优势是在频繁执行后,如果符合优化条件可以被JIT编译进行内联优化提升性能。而native版本完全依赖于native方法NativeMethodAccessorImpl.invoke0()方法,native方法是不能JIT进行内联优化的,所以性能开销较大。

1.3 java版本的MethodAccessor的实现

在设置 -Dsun.reflect.noInflation=true后执行下面例子

public class Animal {
  	/**
     * 小动物可以进行简单的加法运算
     * @param one
     * @param two
     * @return
     */
    public int calculation(int one,int two){
        return (one+two)/0;
    }
}


public class ReflectCallDemo {
    public static void main(String[] args)throws Exception{
       test(new Animal());
    }
    
    public static void test(Animal animal)throws Exception{
        Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
        Object result = eatMethod.invoke(animal, 1, 2);
      	// Thread.sleep(100000);
        System.out.println("1+2="+result);
    }
}

打印异常值如下:
  Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.GeneratedMethodAccessor2.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at jvm.jit.ReflectCallDemo.test(ReflectCallDemo.java:13)
	at jvm.jit.ReflectCallDemo.main(ReflectCallDemo.java:7)
Caused by: java.lang.ArithmeticException: / by zero
	at jvm.jit.Animal.calculation(Animal.java:20)
	... 4 more

由此可见sun.reflect.GeneratedMethodAccessor2就是通过ASM技术动态生成的MethodAccessor的java版本实现,这个实现类利用断点无法跟进去,我们将例子中的**//Thread.sleep(100000);**注释放开,然后通过阿里的JVM调试工具Arthas检测当前main的java进程,找到jvm中已加载的GeneratedMethodAccessor2类并对其进行反编译一探究竟。GeneratedMethodAccessor2的代码如下:

/*
 * Decompiled with CFR.
 * 
 * Could not load the following classes:
 *  jvm.jit.Animal
 */
package sun.reflect;

import java.lang.reflect.InvocationTargetException;
import jvm.jit.Animal;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor2
extends MethodAccessorImpl {
    /*
     * Loose catch block
     */
    public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
        char c;
        char c2;
        Animal animal;
        block18: {
            if (object == null) {
                throw new NullPointerException();
            }
          	// 将object强转成Animal
            animal = (Animal)object;
          	// 参数长度校验
            if (objectArray.length != 2) {
                throw new IllegalArgumentException();
            }
          	// 基础类型参数拆箱(在method.invoke调用侧会进行装箱)
            Object object2 = objectArray[0];
            if (object2 instanceof Byte) {
                c2 = ((Byte)object2).byteValue();
            } else if (object2 instanceof Character) {
                c2 = ((Character)object2).charValue();
            } else if (object2 instanceof Short) {
                c2 = (char)((Short)object2).shortValue();
            } else if (object2 instanceof Integer) {
                c2 = (char)((Integer)object2).intValue();
            } else {
                throw new IllegalArgumentException();
            }
          	// 基础类型参数拆箱(在method.invoke调用侧会进行装箱)
            object2 = objectArray[1];
            if (object2 instanceof Byte) {
                c = ((Byte)object2).byteValue();
                break block18;
            }
            if (object2 instanceof Character) {
                c = ((Character)object2).charValue();
                break block18;
            }
            if (object2 instanceof Short) {
                c = (char)((Short)object2).shortValue();
                break block18;
            }
            if (object2 instanceof Integer) {
                c = (char)((Integer)object2).intValue();
                break block18;
            }
            throw new IllegalArgumentException();
        }
        try {
          	// 调用animal.calculation((int)c2, (int)c);再将返回值基础类型装箱
            return new Integer(animal.calculation((int)c2, (int)c));
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(super.toString());
        }
    }
}

上述代码可知反射java版本的原理其实就是直接调用。

二、反射调用优

大伙都知道反射调用性能比较低,那接下来主要看下反射性能低在什么地方?是否可以通过代码进行优化使其接近于直接调用?

2.1、为什么反射调用性能低

​ 除了我们在上面源码分析过程中遇到的开销外,Method.invoke方法在调用方也存在隐式的开销,可以在字节码中看到端倪。看下面这个例子及其字节码:

java源代码
public class ReflectCallDemo {
    public static void main(String[] args)throws Exception{
       test(new Animal());
    }

    public static void test(Animal animal)throws Exception{
        Method eatMethod = animal.getClass().getDeclaredMethod("calculation", int.class, int.class);
        Object result = eatMethod.invoke(animal, 1, 2);
        //Thread.sleep(1000000000);
    }
}

javap 反编译后的字节码,重点关注29354246
  
 0 aload_0
 1 invokevirtual #5 <java/lang/Object.getClass : ()Ljava/lang/Class;>
 4 ldc #6 <calculation>
 6 iconst_2
 7 anewarray #7 <java/lang/Class>
10 dup
11 iconst_0
12 getstatic #8 <java/lang/Integer.TYPE : Ljava/lang/Class;>
15 aastore
16 dup
17 iconst_1
18 getstatic #8 <java/lang/Integer.TYPE : Ljava/lang/Class;>
21 aastore
22 invokevirtual #9 <java/lang/Class.getDeclaredMethod : (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;>
25 astore_1
26 aload_1
27 aload_0
28 iconst_2
29 anewarray #10 <java/lang/Object>    // 创建Object[]
32 dup
33 iconst_0
34 iconst_1
35 invokestatic #11 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;> // 对int类型的第一个参数进行装箱转换为Integer类型
38 aastore
39 dup
40 iconst_1
41 iconst_2
42 invokestatic #11 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;> // 对int类型的第二个参数进行装箱转换为Integer类型
45 aastore
46 invokevirtual #12 <java/lang/reflect/Method.invoke : (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;>  // 调用Method.invoke方法
49 astore_2
50 return

 

​ 由于invoke接口的第二个参数是Object[],所以在进行反射调前会隐式的创建一个Object[]数组用于存放参数,而且会对数组中基本类型的参数进行隐式的装箱转换。

​ 下面我们结合调用方的字节码案例以及上面的源码分析总结下反射性能低的原因。

  1. 创建Object[]数组耗时,装箱转换耗时,触发GC耗时:由于invoke接口的第二个参数是Object[],所以在进行反射调前会隐式的创建一个Object[]数组用于存放参数,这个数组会占用堆内存;Object[]数组中的基础数据类型会被隐式进行装箱转换,装箱过程中创建的包装类对象会占用堆内存。频繁执行Method.invoke方法后会产生GC。

  2. 方法修饰符权限检查:Method.invoke方法在执行时会针对方法修饰符进行权限检查

  3. native版本无法内联:native版本反射调用实现完全依赖于NativeMethodAccessorImpl.invoke0()这个native方法方法,native方法是不能JIT进行内联优化的,所以性能开销较大。

  4. java版本超多态虚方法无法内联:java版本的反射调用实现是利用ASM技术动态创建MethodAccessorImpl的子类GeneratedMethodAccessor*,由于代码中各处反射都要通过Method.invoke()调用到MethodAccessor.invoke(),所以Method.invoke 中的MethodAccessor.invoke()就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。

    在这里插入图片描述

:虽然Method 没有子类按道理可以被完全去虚化内联,但是由于Method中的MethodAccessor是动态生成的GeneratedMethodAccessor* 存在众多版本,每个版本的方法体都不一样。Method.invoke()方法其实就是委派MethodAccessor即GeneratedMethodAccessor* 执行invoke方法,所以Method.invoke方法本身也存在很多版本,相当于每个不同的方法它的Method.invoke的方法体都是不同的。综上java版本反射调用的实现能否被内联的关键在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用是否可以被去虚化并内联(关于虚方法内联相关可以看1.JVM角度看方法调用-开篇)。

2.2、反射调用性能测试和优化

​ 根据上面的总结我们得到了反射调用性能低的几个原因,然后我们接下来看下那些缺陷可以优化那些是没发避免的。我们分阶段进行优化,每优化一个原因就进行性能测试,看下相较于直接调用的差距。为了证明我们的每个优化操作都是有作用的,我们需要看JIT的内联日志以及GC日志,所以这里我们暂且不用JMH框架进行严谨性能测试,采用手写计时取平均值的方式。后续会专门有一篇文章使用JMH框架针对直接调用、反射调用、MethodHandle调用进行性能测试。

(一)、直接调用
	添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining   
public class TargetCallDemo1 {
    public static void main(String[] args) {
        int count=1;
        long totalTime=0L;
        Animal animal=new Animal();
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
               test(animal);
            }
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

    public static void test(Animal animal){
        animal.calculation(128,129);
    }
}

打印日志:
第1次执行时长:10毫秒 
...20次执行时长:0毫秒 
反射调用平均时长:1毫秒

根据GC日志可知,上例中直接调用不会触发GC。

根据JIT内联日志可以知,上例中 animal.calculation(128,129);被成功内联。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zjtC47Gh-1653492260409)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220524220142596.png)]

总结:

​ 直接调用刚开始在JIT分层编译尚未完全稳定前会有大概10毫秒作有的耗时,分层编译结束后 animal.calculation(128,129)被JIT内联优化,直接调用耗时可以忽略不计。

(二)、反射调用
	添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining 
public class ReflectCallDemo1 {
    public static void main(String[] args)throws Exception{
        int count=1;
        long totalTime=0L;
        Animal animal=new Animal();
        Method eatMethod = animal.getClass().getDeclaredMethod("calculation", int.class, int.class);
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
                test(eatMethod,animal);
            }
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

    public static void test(Method method,Animal animal)throws Exception{
        method.invoke(animal,128,129);
    }

}

打印日志:
第1次执行时长:902毫秒
...20次执行时长:722毫秒
反射调用平均时长:753毫秒

根据GC日志可知,上例中反射调用会触发GC。

根据JIT内联日志可以知,上例中 method.invoke(animal,128,129)方法中MethodAccessor即GeneratedMethodAccessor* 执行invoke方法被成功内联。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4BkFgvlo-1653492260410)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220524222407402.png)]

​ 总结:

​ 上述例子未优化的反射调用有 2.1中描述的1/2/3种缺陷,导致相比于直接调用差距较大,仅依据此例子来看反射调用和直接调用相差750倍之余。

(三)、反射调用优化版本1

​ 首先我们优化下缺陷1.创建Object[]数组耗时,装箱转换耗时,触发GC耗时,优化版本如下,将calculation()方法的入参又128,129修改成2,3。Integer只能缓存[-128,127]直接的Integer对象,128和129不在范围内,在执行Integer.valueOf装箱时每次都会创建新对象。添加-XX:InlineSmallCode=1000000,保证test()方法下整个调用链被全部内联,全部内联后Object[]数据会被判断为不可逃逸,此时JIT会采用栈分配的方式创建Object[]不会占用堆内存。

添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining -XX:InlineSmallCode=1000000
public class ReflectCallDemo1 {
    public static void main(String[] args)throws Exception{
        int count=1;
        long totalTime=0L;
        Animal animal=new Animal();
        Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
                test(eatMethod,animal);
            }
            //Thread.sleep(10000);
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

    public static void test(Method method,Animal animal)throws Exception{
        method.invoke(animal,2,3);
    }
  
  
  打印日志:
第1次执行时长:362毫秒
...20次执行时长:268毫秒
反射调用平均时长:277毫秒

根据GC日志可知,上例中反射调用未触发GC。

根据JIT内联日志可以知,上例中 method.invoke(animal,2,3)方法中MethodAccessor即GeneratedMethodAccessor* 执行invoke方法被成功内联。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9PFL9Tsn-1653492260410)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220525182620547.png)]

​ 总结:

​ 上述例子优化了1.创建Object[]数组耗时,装箱转换耗时,触发GC耗时这个缺陷,耗时有明显提升,仅依据此例子来看反射调用和直接调用相差270倍之余。

(四)、反射调用优化版本2

​ 在上例的基础上再优化2.方法修饰符权限检查,在创建Method后设置Method.setAccessible(true)取消权限检查。

添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining -XX:InlineSmallCode=1000000
public class ReflectCallDemo1 {
    public static void main(String[] args)throws Exception{
        int count=1;
        long totalTime=0L;
        Animal animal=new Animal();
        Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
        eatMethod.setAccessible(true);//取消权限检查
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
                test(eatMethod,animal);
            }
            //Thread.sleep(10000);
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

    public static void test(Method method,Animal animal)throws Exception{
        method.invoke(animal,2,3);
    }

}
  打印日志:
第1次执行时长:239毫秒
...20次执行时长:196毫秒
反射调用平均时长:199毫秒

根据GC日志可知,上例中反射调用未触发GC。

根据JIT内联日志可以知,上例中 method.invoke(animal,2,3)方法中MethodAccessor即GeneratedMethodAccessor* 执行invoke方法被成功内联。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kzOYF7g-1653492260411)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220525184150059.png)]

​ 总结:

​ 上述例子优化了1.创建Object[]数组耗时,装箱转换耗时,触发GC耗时2.方法修饰符权限检查这两个缺陷,相对于只优化1提升不是很明显,仅依据此例子来看反射调用和直接调用相差200倍之余。

(五)、反射调用优化版本3

​ 在上例的基础上再优化3.native版本无法内联,上述反射调用优化版本1、2中全部都是先native版本实现运行15次后转换为java版本实现,然后运行一段时间后变为热点代码被JIT内联优化。由于native版本向java版本切换的过程中针对DelegatingMethodAccessorImpl.invoke()方法中的delegate,会收集到两个类型的profile(NativeMethodAccessorImpl和GeneratedMethodAccessor*),这会增加JIT内联优化的复杂度,推迟内联优化时间。现添加参数-Dsun.reflect.noInflation=true ,可以关闭反射调用的 Inflation 机制,从而取消委派实现,并且直接使用java版本实现,可以使JIT提前编译完成,也可以做更加激进的优化。

添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining -XX:InlineSmallCode=1000000  -Dsun.reflect.noInflation=true
  
  例子和上例相同只是增加一个参数
  
  
  
  
  打印日志:
第1次执行时长:153毫秒
...20次执行时长:132毫秒
反射调用平均时长:135毫秒
  

根据GC日志可知,上例中反射调用触发GC。

根据JIT内联日志可以知,上例中 method.invoke(animal,2,3)方法中MethodAccessor即GeneratedMethodAccessor* 执行invoke方法被成功内联

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tJUmb2jy-1653492260411)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220525201616863.png)]

​ 总结:

​ 上述例子优化了1.创建Object[]数组耗时,装箱转换耗时,触发GC耗时2.方法修饰符权限检查3.native版本无法内联这三个缺陷,相对于只优化1、2提升不是很明显,仅依据此例子来看反射调用和直接调用相差130倍之余。

(六)、MethodHandle的黑魔法

​ 针对上例的场景我们已经将反射调用优化到了极致,相比于直接调用仍然相差较大,接下来我们换成MethodHandle试试。

添加JVM参数:-XX:+PrintGC -XX:+UnlockDiagnosticVMOptions  -XX:-TieredCompilation  -XX:+PrintInlining -XX:InlineSmallCode=1000000  -Dsun.reflect.noInflation=true
public class ReflectCallDemo1 {
   static  MethodHandle methodHandle;
   static {

       try {
           Animal animal=new Animal();
           Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
           MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
           methodHandle= publicLookup.unreflect(eatMethod).bindTo(animal);
       } catch (Throwable e) {
           e.printStackTrace();
       }
    }

    final static  MethodHandle finalMethodHandle=methodHandle;

    public static void main(String[] args)throws Throwable{
        int count=1;
        long totalTime=0L;
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
                test();
            }
            //Thread.sleep(10000);
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

        public static void test()throws Throwable{
            int aa= (int) finalMethodHandle.invokeExact(2, 3);
        }

}


 打印日志:
第1次执行时长:12毫秒
...20次执行时长:0毫秒
反射调用平均时长:1毫秒

Amazing!!!!!,MethodHandle调用的性能竟然和直接调用不相上下。关于MethodHandle性能为什么直逼直接调用?为什么MethodHandle必须被final修饰?在超多态虚方法调用时MethodHandle性能是否能超越直接调用?这些问题再下一篇3.JVM角度看方法调用-MethodHandle篇中会有讲解。

(七)、java版本超多态虚方法无法内联问题

​ 我们上面的全部优化案例中只优化了反射调用性能低的缺陷1.创建Object[]数组耗时,装箱转换耗时,触发GC耗时2.方法修饰符权限检查3.native版本无法内联这三个缺陷。始终没有提到关于4.java版本超多态虚方法无法内联,这是因为我们为了案例简单上述场景中只涉及Animal.calculation(int one,int two)这一个Method的高频调用,所以就不存在java版本的MethodAccessor实现即GeneratedMethodAccessor* 执行invoke方法时无法被内联的问题了,说白了就是没踩到这个坑。下面我们看下在测试程序前增加一下其他类型Method执行invoke方法,创造一下超多态的情况。

public class ReflectCallDemo1 {
    public static void main(String[] args)throws Exception{
        int count=1;
        long totalTime=0L;
        Animal animal=new Animal();
        // 添加其他类型的Method
        polluteProfile();
        Method eatMethod = animal.getClass().getMethod("calculation", int.class, int.class);
        eatMethod.setAccessible(true);//取消权限检查
        while(count<=20){
            long start=System.currentTimeMillis();
            for(int i=0;i<100_000_000;i++){
                test(eatMethod,animal);
            }
            //Thread.sleep(10000);
            long result=System.currentTimeMillis()-start;
            System.out.println("第"+count+"次执行时长:"+result+"毫秒");
            totalTime=totalTime+result;
            count++;
        }
        System.out.println("反射调用平均时长:"+totalTime/20+"毫秒");
    }

    private static void polluteProfile()throws Exception{
        Method method1 = String.class.getMethod("toString");
        Method method2 = String.class.getMethod("trim");
        Method method3=Integer.class.getMethod("intValue");
        String str="123";
        Integer number=123;
        for(int i=0;i<300_000_000;i++){
            method1.invoke(str);
            method2.invoke(str);
            method3.invoke(number);
        }
    }

    public static void test(Method method,Animal animal)throws Exception{
        method.invoke(animal,2,3);
    }

}


 打印日志,明显时间加长了:
第1次执行时长:1085毫秒
...20次执行时长:1068毫秒
反射调用平均时长:1053毫秒

  
根据GC日志可知,上例中反射调用会触发GC。

根据JIT内联日志可以知,上例中  method.invoke(animal,2,3)方法中MethodAccessorGeneratedMethodAccessor* 执行invoke方法未被成功内联。此处内联失败就是因为MethodAccessor.invoke()这个调用点上MethodAccessor存在四个类型,是超多态情况无法去虚化导致的内联失败。
  注:HotSpot针对虚方法进行去虚化优化的缓存profile收集条数(由JVM参数 -XX:TypeProfileWidth 控制)默认是2


接下来当我们修改下polluteProfile()方法,注释掉两个方法调用

 private static void polluteProfile()throws Exception{
        Method method1 = String.class.getMethod("toString");
        String str="123";
        for(int i=0;i<300_000_000;i++){
            method1.invoke(str);
        }
    }

此时在执行上述例子,输出结果如下,明显时间又降下来了:

1次执行时长:198毫秒
...20次执行时长:162毫秒
反射调用平均时长:169毫秒
  

根据GC日志可知,上例中反射调用触发GC。

根据JIT内联日志可以知,上例中 method.invoke(animal,2,3)方法中MethodAccessor即GeneratedMethodAccessor* 执行invoke方法被成功内联。由于HotSpot针对虚方法进行去虚化优化的缓存profile收集条数(由JVM参数 -XX:TypeProfileWidth 控制)默认是2,所以此处可以进行条件去虚化并且内联。此处的GeneratedMethodAccessor2和GeneratedMethodAccessor3分别代表String.toString()的实现和Animal.calculation()的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mCtReVnd-1653492260411)(/Users/yangxiaofei/Library/Application Support/typora-user-images/image-20220525213929697.png)]

Method.invoke()方法被JIT条件去虚化内联后的伪代码如下:

 public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
           
        // 开始
        if(obj.getClass()==String.class){
         		//...省略GeneratedMethodAccessor2中的拆箱
           return (String)obj.toString();
        }else if(obj.getClass()==Animal.class){
          //...省略GeneratedMethodAccessor3中的拆箱
          return (Animal)obj.calculation((int)args[0],(int)args[1]);
        }else{
           return ma.invoke(obj, args);
        }   
 
    }
(八)、总结

​ 反射的前三种缺陷我们还可以通过代码优化解决,但是第四种也是最致命的一个缺陷,它是没办法避免的。所有的反射调用都要对应一个Method实例,所有处于热点代码的Method实例在执行invoke时都会产生一个MethodAccessor类型的实现即GeneratedMethodAccessor* 。所以Method.invoke 中的MethodAccessor.invoke()就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方,进而影响热点代码的性能。

​ 所以当我们热点代码需要用到反射调用时不妨尝试用MethodHandle代替。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

躺平程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值