2

1. 前言

  之前写过一篇java反射的文章《Java反射及性能》,但是感觉不是很完整,所以想把这块内容好好再梳理记录一下。

2. 简单的demo

  • 本案例针对JDK1.8

常规的自定义反射使用,测试代码:

【TestRef.java】
public class TestRef {
 
    public static void main(String[] args) {
        try {
            Class<?> clazz = Class.forName("com.allen.commons.entity.CommonTestEntity");
            Object refTest = clazz.newInstance();
            Method method = clazz.getMethod("defaultMethod");
            //Method method1 = clazz.getDeclaredMethod("defaultMethod");
            method.invoke(refTest);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
 
    }
}
---------------------------------------------------------------------------------------
【CommonTestEntity.java】
public class CommonTestEntity {
 
    static {
        System.out.println("CommonTestEntity执行类加载...");
    }
 
    public CommonTestEntity() {
        System.out.println(this.getClass() + " | CommonTestEntity实例初始化 | " + this.getClass().getClassLoader());
    }
 
    public void defaultMethod() {
        System.out.println("执行实例方法:defaultMethod");
    }
}

3. jdk反射之Method获取

3.1. jdk反射大致的使用流程

  1. 创建class对象(类加载,使用当前方法所在类的ClassLoader来加载)
  2. 获取目标Method对象「反射调用的目标方法」(getMethod 和 getDeclaredMethod)
  3. 调用Method的invoke方法

3.2. getMethod 和 getDeclaredMethod区别

getMethod源码:

public Method getMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        Objects.requireNonNull(name);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // 1. 检查方法权限
            checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
        }
        // 2. 获取方法
        Method method = getMethod0(name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(methodToString(name, parameterTypes));
        }
        // 3. 返回方法
        return method;
    }

getDeclaredMethod源码:

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        Objects.requireNonNull(name);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // 1. 检查方法是权限
            checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
        }
        // 2. 获取方法
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(methodToString(name, parameterTypes));
        }
        // 3. 返回方法
        return method;
}
  • 获取方法的流程总结为三步走:
    a.检查方法权限
    b.获取方法 Method 对象
    c.返回方法

3.2.1. 主要的两个区别

  1. getMethod 中 checkMemberAccess 传入的是 Member.PUBLIC,而 getDeclaredMethod 传入的是 Member.DECLARED 。代码中的注释:
    image.png
    注释里解释了 PUBLICDECLARED 的不同,PUBLIC 会包括所有的 public 方法,包括父类的方法,而 DECLARED 会包括所有自己定义的方法,public,protected,private 都在此,但是不包括父类的方法。
     
  2. getMethod 中获取方法调用的是 getMethod0,而 getDeclaredMethod 获取方法调用的是 privateGetDeclaredMethods 。privateGetDeclaredMethods 是获取类自身定义的方法,参数是 boolean publicOnly,表示是否只获取公共方法。其中getDeclaredMethod源码如下【publicOnly为false】:
    image.png

    • getDeclaredMethod其实是从privateGetDeclaredMethods返回的方法列表里复制一个Method对象返回。复制的过程是在searchMethods里实现的,源码如下:

    image.png
     
    privateGetDeclaredMethods 源码如下:

    // Returns an array of "root" methods. These Method objects must NOT
     // be propagated to the outside world, but must instead be copied
     // via ReflectionFactory.copyMethod.
     private Method[] privateGetDeclaredMethods(boolean publicOnly) {
         checkInitted();
         Method[] res;
         // 默认先从缓存获取
         ReflectionData<T> rd = reflectionData();
         if (rd != null) {
             // 走rd.declaredMethods分支
             res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
             if (res != null) return res;
         }
         // No cached value available; request value from VM
         res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
         if (rd != null) {
             if (publicOnly) {
                 rd.declaredPublicMethods = res;
             } else {
                 rd.declaredMethods = res;
             }
         }
         return res;
     }

    ① 首先通过relectionData 缓存获取
    ② 如果缓存没有命中的话,通过 getDeclaredMethods0 通过native方法从VM中获取方法
    从上面的源码我们可以看到,在获取反射方法的时候Class对象会有一个叫ReflectionData的缓存,关于这个缓存也是颇有些东西在里面,下面会有单独一小节详细介绍。
     

  3. getMethod0源码如下:

    private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) {
         MethodArray interfaceCandidates = new MethodArray(2);
         Method res =  privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates);
         if (res != null)
             return res;
     
         // Not found on class or superclass directly
         interfaceCandidates.removeLessSpecifics();
         return interfaceCandidates.getFirst(); // may be null
     }

    其中privateGetMethodRecursive方法中也会调用到privateGetDeclaredMethods方法和searchMethods方法。

3.3. getMethod及getDeclaredMethod方法调用流程图

反射(直接反射)方法调用流程.png

3.4. reflectionData

  是Class中的一个缓存类,主要缓存的是每次从jvm里获取到的一些类属性,比如方法,字段等。

image.png

privateGetDeclaredMethods每次在获取Method对象的时候,首先会查询reflectionData缓存,如果有就直接返回,没有就从VM中查询目标Method,并缓存至reflectionData

  从源码中可以知道,Class中的这个属性是SoftReference的,也就是在内存比较紧张的情况下可能会被回收。JVM可以通过-XX:SoftRefLRUPolicyMSPerMB这个参数来控制回收的时机,一旦回收条件达成,只要触发GC,就会回收SoftReference引用对象。
  如果reflectionData回收了,之后调用privateGetDeclaredMethods就会重新创建一个ReflectionData对象,当然也需要重新从VM中获取Method相关数据,reflectionData关联的Method,Field字段等都会重新生成新的对象。

3.4. method获取的性能问题

  常规的反射调用到这边已经走过了一半的流程,讲到这边,有必要先进行一下小结。因为在这上半部分的流程中,就涉及到常规jdk反射的性能问题。

  在常规的反射调用中,目标method的获取是十分消耗性能的,原因是:

  1. 首先需要checkAccess,检查方法可见性;
  2. 需要遍历方法并校验参数,主要涉及的方法为searhMethods;【同时relectionData缓存失效后需要调用native方法从VM中获取方法并重新添加至缓存】
  3. searhMethods每次都会copy一个Method的备份然后返回;

针对反射上半部分流程的优化

  1. 缓存需要反射调用的目标方法;
  2. 缓存class对象,不要频繁的进行类加载操作;「性能极差

坊间传闻,缓存目标Method对象,反射耗时大约是直接调用的25倍。如果不做缓存,反射耗时大约是直接调用的60倍。本人没有实际测试过😥。

4. jdk反射之invoke方法

  先看invoke源码:

class Method {
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            // 1. 检查权限
            checkAccess(caller, clazz,
                        Modifier.isStatic(modifiers) ? null : obj.getClass(),
                        modifiers);
        }
        // 2. 获取 MethodAccessor
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            // 创建 MethodAccessor
            ma = acquireMethodAccessor();
        }
        // 3. 调用 MethodAccessor.invoke
        return ma.invoke(obj, args);
    }
}

4.1. Method.invoke

Method部分源码:

public final class Method extends Executable {
    private Class<?>            clazz;
    private String              name;
    private volatile MethodAccessor methodAccessor;
    private Method              root;
    @CallerSensitive
    public Object invoke(Object obj, Object... args)
}

4.1.1 root

  每个实际的Java方法只有一个对应的Method对象作为root(实质上就是Method类的一个成员变量),root指向reflectionData里缓存的某个方法。每次在通过反射获取Method对象时新创建Method对象把root封装起来,其实就是前文讲getDeclaredMethod提到的方法copy操作,copy的具体源码如下:
image.png

4.1.2 MethodAccessor

Method.invoke()实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor来处理。
在第一次调用invoke()方法的时候,实现调用逻辑的MethodAccessor对象才会新建并更新给root,然后调用MethodAccessor.invoke()真正完成反射调用。

  • 第一次调用Method.invoke()的时候会检查并获取MethodAccessor
    image.png
     
  • 获取的MethodAccessor其实都是从root Method获取的,如果root中的MethodAccessor为null,则进行新建。
    image.png

MethodAccessor只是单方法接口,其invoke()方法与Method.invoke()的对应。

public interface MethodAccessor {
    Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException;
}

MethodAccessor主要有三种实现

  1. DelegatingMethodAccessorImpl
  2. NativeMethodAccessorImpl
  3. GeneratedMethodAccessorXXX

其中DelegatingMethodAccessorImpl是最终注入给Method的methodAccessor的,也就是Method所有的invoke方法都会调用这个DelegatingMethodAccessorImpl.invoke,正如其名,这个类是做代理的,其真正的实现是下面的两种:NativeMethodAccessorImplGeneratedMethodAccessorXXX

创建MethodAccessor实例的是ReflectionFactory,源码如下:
image.png

NativeMethodAccessorImpl是native code实现的,GeneratedMethodAccessorXXX是java版本实现的,需要为反射调用的Method动态生成新类,XXX是连续递增的数字。

所有的方法反射都先调用NativeMethodAccessorImplNativeMethodAccessorImpl调用15次之后会生成一个GeneratedMethodAccessorXXX类,转而使用java版本的invoke。这块逻辑可以看NativeMethodAccessorImpl源码:
image.png

上图中new MethodAccessorGenerator().generateMethod就是动态创建字节码并进行实例化的逻辑。

第一次创建GeneratedMethodAccessorXXX类并实例化的时候,因为涉及类加载操作,【这块类加载的逻辑是:每个GeneratedMethodAccessorXXX类都会实例化一个DelegatingClassLoader类加载器进行类加载(方便GeneratedMethodAccessorXXX类卸载)】,性能会有所影响;
  • 动态生成的新类如下:
package sun.reflect;
// 这是反射目标方法所在的类
import com.allen.entities.CommonTestEntity;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1
        extends MethodAccessorImpl {
    /*
     * Loose catch block
     */
    public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
        CommonTestEntity commonTestEntity;
        block5:
        {
            if (object == null) {
                throw new NullPointerException();
            }
            commonTestEntity = (CommonTestEntity) object;
            if (objectArray == null || objectArray.length == 0) break block5;
            throw new IllegalArgumentException();
        }
        try {
            // 执行真正的目标方法
            commonTestEntity.defaultMethod();
            return null;
        } catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        } catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(super.toString());
        }
    }
}

而15次的阈值在ReflectionFactory中定义了
image.png

  • invoke 超过15次调用后创建GeneratedMethodAccessorXXX的行为也叫反射膨胀,这个膨胀阈值可以自定义设置也可直接关闭膨胀功能。通过-Dsun.reflect.noInflation=true-Dsun.reflect.inflationThreshold=0可以直接关闭膨胀功能,-Dsun.reflect.inflationThreshold配置也能自定义配置膨胀阈值。
  • -Dsun.reflect.inflationThreshold最大值可以配置为Integer.MAX,通过增大该阈值或者直接关闭膨胀功能,可以解决频繁创建GeneratedMethodAccessorXXX字节码和元空间内存问题。相关案例可见:《Potential native memory use in reflection delegating classloaders》

  Java 版本的 MethodAccessorImpl 调用效率比 Native 版本要快 20 倍以上【纳秒级别,无法感知】,但是 Java 版本加载时要比 Native 多消耗 3-4 倍资源,所以默认会调用 Native 版本,如果调用次数超过 15 次以后,就会选择运行效率更高的 Java 版本。

4.2. invoke的性能问题

4.2.1. JIT 无法优化

Oracle官方文档提到:
image.png
 

4.2.2. Method#invoke方法会对参数做封装和解封操作

invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型(8中基本数据类型)的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。
 

4.2.3. 增加GC压力

  1. 封装和解封,产生了额外的不必要的内存浪费
  2. 频繁的反射调用可能生成大量的GeneratedMethodAccessorXXX类并创建很多DelegatingClassLoader类加载器,占用堆内存的同时也会增加元空间的占用【同时引起元空间的碎片化】,引发更多的GC行为。

开翻挖掘机
225 声望26 粉丝

不忘初心❤️,且行且思考