反射

Java 反射效率低主要原因是:

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

2 需要检查方法可见性

3 需要校验参数

4 反射方法难以内联

5 JIT 无法优化

代码块


public class Reflect {
​
    public static void main(String[] args) throws Exception {
        Proxy target = new Proxy();
        Method method = Proxy.class.getDeclaredMethod("run");
        method.invoke(target);
    }
​
    static class Proxy {
        public void run() {
            System.out.println("run");
        }
    }
}

调用Class类的getDeclaredMethod可以获取指定方法名和参数的方法对象Method。

 

getDeclaredMethod 方法 与 getMethod 方法区别:

1 PUBLIC 会包括所有的 public 方法,包括父类的方法,

2 而 DECLARED 会包括所有自己定义的方法,public,protected,private 都在此,但是不包括父类的方法。

代码块

/**
     * Identifies the set of all public members of a class or interface,
     * including inherited members.
     */
    public static final int PUBLIC = 0;
​
    /**
     * Identifies the set of declared members of a class or interface.
     * Inherited members are not included.
     */
    public static final int DECLARED = 1;

 

getMethod方法:

代码块

public Method getMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        // 检查方法权限
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
         // 获取方法 Method 对象   返回方法的拷贝
        Method method = getMethod0(name, parameterTypes, true);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }
    
 

 

Method method = getMethod0(name, parameterTypes, true);

我们先看看检查方法权限做了些什么事情。

代码块

private void checkMemberAccess(int which, Class<?> caller, boolean checkProxyInterfaces) {
        final SecurityManager s = System.getSecurityManager();
        if (s != null) {
            /* Default policy allows access to all {@link Member#PUBLIC} members,
             * as well as access to classes that have the same class loader as the caller.
             * In all other cases, it requires RuntimePermission("accessDeclaredMembers")
             * permission.
             */
            final ClassLoader ccl = ClassLoader.getClassLoader(caller);
            final ClassLoader cl = getClassLoader0();
            if (which != Member.PUBLIC) {
                if (ccl != cl) {
                    s.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
                }
            }
            this.checkPackageAccess(ccl, checkProxyInterfaces);
        }
    }
    
 


在这里可以看到,对于非 Member.PUBLIC 的访问,会增加一项检测,SecurityManager.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION); 这项检测需要运行时申请 RuntimePermission("accessDeclaredMembers")。

这里就不继续往下看了,方法整体是在检查是否可以访问对象成员。

接着看下是如何获取方法的 Method 对象。

 

getMethod0:

代码块

private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) {
        MethodArray interfaceCandidates = new MethodArray(2);
        //获取到 MethodList 对象
        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
    }
    
 

这里是通过 getMethodsRecursive 获取到 MethodList 对象,然后通过 MethodList#getMostSpecific 方法筛选出对应的方法。 MethodList#getMOstSpecific 会筛选返回值类型最为具体的方法,至于为什么会有返回值的区别,后面会讲到。

(这里的具体,指的是有两个方法,返回值分别是 Child 和 Parent,Child 继承自 Parent,这里会筛选出返回值为 Child 的方法)。

接着看 getMethodsRecursive 方法,是如何获取方法的。

 

getMethodsRecursive:

代码块

private Method privateGetMethodRecursive(String name,
            Class<?>[] parameterTypes,
            boolean includeStaticMethods,
            MethodArray allInterfaceCandidates) {
        Method res;
        // Search declared public methods   获取自己所有的 public 方法   查找 方法名,参数相同的方法
        if ((res = searchMethods(privateGetDeclaredMethods(true),
                                 name,
                                 parameterTypes)) != null) {
            //如果找到,直接返回
            if (includeStaticMethods || !Modifier.isStatic(res.getModifiers()))
                return res;
        }
        // Search superclass's methods
        //如果自己没有实现对应的方法,就去父类中查找对应的方法查找接口中对应的方法通过上面四个步骤,最终获取到的是一个 MethodList 对象,
        //是一个链表结点,其 next 指向下一个结点
        if (!isInterface()) {
            Class<? super T> c = getSuperclass();
            if (c != null) {
                if ((res = c.getMethod0(name, parameterTypes, true)) != null) {
                    return res;
                }
            }
        }
        // Search superinterfaces' methods
        Class<?>[] interfaces = getInterfaces();
        for (Class<?> c : interfaces)
            if ((res = c.getMethod0(name, parameterTypes, false)) != null)
                allInterfaceCandidates.add(res);
        // Not found
        return null;
    }
    
 

这里获取方法有四个步骤:

通过 privateGetDeclaredMethods 获取自己所有的 public 方法通过 MethodList#filter 查找 方法名,参数相同的方法,如果找到,直接返回如果自己没有实现对应的方法,就去父类中查找对应的方法查找接口中对应的方法通过上面四个步骤,最终获取到的是一个 MethodList 对象,是一个链表结点,其 next 指向下一个结点。也就是说,这里获取到的 Method 会有多个。

这里稍微解释一下,在我们平时编写 Java 代码时,同一个类是不能有方法名和方法参数都相同的方法的,而实际上,在 JVM 中,一个方法签名是和 返回值,方法名,方法参数 三者相关的。 也就是说,在 JVM 中,可以存在 方法名和方法参数都相同,但是返回值不同的方法。

所以这里返回的是一个方法链表。

所以上面最终返回方法时会通过 MethodList#getMostSpecific 进行返回值的筛选,筛选出返回值类型最具体的方法。

这里我们先暂停回顾一下整体的调用链路:

getMethod -> getMethod0 -> getMethodsRecursive -> privateGetDeclaredMethods通过函数调用,最终会调用到 privateGetDeclaredMethods 方法,也就是真正获取方法的地方。

 

privateGetDeclaredMethods:

代码块

private Method[] privateGetDeclaredMethods(boolean publicOnly) {
        checkInitted();
        Method[] res;
        //relectionData 通过缓存获取
        ReflectionData<T> rd = reflectionData();
        if (rd != null) {
            res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
            if (res != null) return res;
        }
        // No cached value available; request value from VM
        //如果缓存没有命中的话,通过 getDeclaredMethods0 获取方法  从jvm获取
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
        if (rd != null) {
            if (publicOnly) {
                rd.declaredPublicMethods = res;
            } else {
                rd.declaredMethods = res;
            }
        }
        return res;
    }
    
 

 

在 privateGetDeclaredMethods 获取方法时,有两个步骤:

1relectionData 通过缓存获取

2如果缓存没有命中的话,通过 getDeclaredMethods0 获取方法

代码块

private ReflectionData<T> reflectionData() {
        SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
        int classRedefinedCount = this.classRedefinedCount;
        ReflectionData<T> rd;
        if (useCaches &&
            reflectionData != null &&
            (rd = reflectionData.get()) != null &&
            rd.redefinedCount == classRedefinedCount) {
            return rd;
        }
        // else no SoftReference or cleared SoftReference or stale ReflectionData
        // -> create and replace new instance
        return newReflectionData(reflectionData, classRedefinedCount);
    }
 

在 Class 中会维护一个 ReflectionData 的软引用,作为反射数据的缓存。

 

ReflectionData 结构如下:

代码块

private static class ReflectionData<T> {
        volatile Field[] declaredFields;
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;
        volatile Constructor<T>[] publicConstructors;
        // Intermediate results for getFields and getMethods
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        volatile Class<?>[] interfaces;
​
        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;
​
        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }
 


可以看到,保存了 Class 中的属性和方法。 如果缓存为空,就会通过 getDeclaredMethods0 从 JVM 中查找方法。

getDeclaredMethods0 是一个 native 方法,这里暂时先不看。

通过上面几个步骤,就获取到 Method 数组。

这就是 getMethod 方法的整个实现了。

 

searchMethods方法将从返回的方法列表里找到一个匹配名称和参数的方法对象。

3 searchMethods

代码块

private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            //比较方法名 method的 name一定在常量池中  所以仅仅比较内存地址即可?
            if (m.getName() == internedName
                //比较方法参数
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                //比较返回值
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }
        //重新copy一份返回
        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }
 

 

Method#copy:

在获取到对应方法以后,并不会直接返回,而是会通过 getReflectionFactory().copyMethod(method); 返回方法的一个拷贝。

最终调用的是 Method#copy,我们来看看其实现。


会 new 一个 Method 实例并返回。

这里有两点要注意:

设置 root = this会给 Method 设置 MethodAccessor,用于后面方法调用。

也就是所有的 Method 的拷贝都会使用同一份 methodAccessor。通过上面的步骤,就获取到了需要反射的方法

 

调用反射方法:

获取到方法以后,通过 Method#invoke 调用方法:

代码块

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
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            //如果 ma 为空,就去创建 MethodAccessor。
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
 

invoke 方法的实现,分为三步:

2.1 检查是否有权限调用方法

这里对 override 变量进行判断,如果 override == true,就跳过检查 我们通常在 Method#invoke 之前,会调用 Method#setAccessible(true),就是设置 override 值为 true。

2.2 获取 MethodAccessor

在上面获取 Method 的时候我们讲到过,Method#copy 会给 Method 的 methodAccessor 赋值。所以这里的 methodAccessor 就是拷贝时使用的 MethodAccessor。

如果 ma 为空,就去创建 MethodAccessor。

代码块

private MethodAccessor acquireMethodAccessor() {
        // First check to see if one has been created yet, and take it
        // if so
        MethodAccessor tmp = null;
        //查找 root 的 MethodAccessor
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            //没有找到,就去创建 MethodAccessor。
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }
​
        return tmp;
    }
 

这里会先查找 root 的 MethodAccessor,这里的 root 在上面 Method#copy 中设置过。

如果还是没有找到,就去创建 MethodAccessor。

这里可以看到,一共有三种 MethodAccessor。MethodAccessorImpl,NativeMethodAccessorImpl,DelegatingMethodAccessorImpl。

采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会 采用 MethodAccessorImpl。

所以默认会调用 NativeMethodAccessorImpl。

MethodAccessorImpl是通过动态生成字节码来进行方法调用的,是 Java 版本的 MethodAccessor,字节码生成比较复杂,这里不放代码了。大家感兴趣可以看这里的 generate 方法。

DelegatingMethodAccessorImpl就是单纯的代理,真正的实现还是 NativeMethodAccessorImpl。

NativeMethodAccessorImpl是 Native 版本的 MethodAccessor 实现。

在 NativeMethodAccessorImpl 的实现中,我们可以看到,有一个 numInvocations 阀值控制,numInvocations 表示调用次数。如果 numInvocations 大于 15(默认阀值是 15),那么就使用 Java 版本的 MethodAccessorImpl。

为什么采用这个策略呢,可以 JDK 中的注释:

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

那为什么 Native 版本运行效率会没有 Java 版本高呢?从 R 大博客来看,是因为 这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

2.3 调用 MethodAccessor#invoke 实现方法的调用

在生成 MethodAccessor 以后,就调用其 invoke 方法进行最终的反射调用。

这里我们对 Java 版本的 MethodAccessorImpl 做个简单的分析,Native 版本暂时不做分析。

在前面我们提到过 MethodAccessorImpl 是通过 MethodAccessorGenerator#generate 生成动态字节码然后动态加载到 JVM 中的。

其中生成 invoke 方法字节码的是 MethodAccessorGenerator#emitInvoke。

我们看其中校验参数的一小段代码:

通过上面的注释以及字节码,我们可以看到,生成的 invoke 方法,会对传入的参数做校验,其中会涉及到 unboxing 操作。

到此,基本上 Java 方法反射的原理就介绍完了。

 

 

进入 Method method = Proxy.class.getDeclaredMethod("run"); 方法

2 getDeclaredMethod

代码块

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        // 检查方法权限
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        // 获取方法 Method 对象   返回方法的拷贝
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }
    
 

1 检查方法权限

2 获取方法 Method 对象

3 返回方法的拷贝

其中privateGetDeclaredMethods方法从缓存或JVM中获取该Class中申明的方法列表,

searchMethods方法将从返回的方法列表里找到一个匹配名称和参数的方法对象。

3 searchMethods

代码块

private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            //比较方法名 method的 name一定在常量池中  所以仅仅比较内存地址即可?
            if (m.getName() == internedName
                //比较方法参数
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                //比较返回值
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }
        //重新copy一份返回
        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }
 

 

 

 

三、Java 反射效率低的原因

了解了反射的原理以后,我们来分析一下反射效率低的原因。

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

我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。

而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。

因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。

2. 需要检查方法可见性

通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)

3. 需要校验参数

反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);

4. 反射方法难以内联

Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。参见 www.iteye.com/blog/rednax…

5. JIT 无法优化

在 JavaDoc 中提到:

Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.

因为反射涉及到动态加载的类型,所以无法进行优化。

 

http://baijiahao.baidu.com/s?id=1647737426571712796&wfr=spider&for=pc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值