编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

4.1反射性能探索和优化

目录介绍

  • 01.反射实用场景
  • 02.尽量远离反射
  • 03.反射的弊端
  • 04.看一个案例分析
  • 05.反射效率低原因
  • 06.getMethod源码
  • 07.invoke()源码
  • 08.总结一下分析

01.反射实用场景

  • 反射的适用场景是什么?
  • 1)Java的反射机制在做基础框架的时候非常有用,有一句话这么说来着:反射机制是很多Java框架的基石。而一般应用层面很少用,不过这种东西,现在很多开源框架基本都已经给你封装好了,自己基本用不着写。典型的除了之外,还有Spring也用到很多反射机制。经典的就是在xml文件或者properties里面写好了配置,然后在Java类里面解析xml或properties里面的内容,得到一个字符串,然后用反射机制,根据这个字符串获得某个类的Class实例,这样就可以动态配置一些东西,不用每一次都要在代码里面去new或者做其他的事情,以后要改的话直接改配置文件,代码维护起来就很方便了,同时有时候要适应某些需求,Java类里面不一定能直接调用另外的方法,这时候也可以通过反射机制来实现。
  • 2)当你做一个软件可以安装插件的功能,你连插件的类型名称都不知道,你怎么实例化这个对象呢?因为程序是支持插件的(第三方的),在开发的时候并不知道 。所以无法在代码中 New出来 ,但反射可以,通过反射,动态加载程序集,然后读出类,检查标记之后再实例化对象,就可以获得正确的类实例。
  • 3)在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象.反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。很多工厂模式就是使用的反射。

02.尽量远离反射

  • 反射:在流行的库如Spring和Gson,Retrofit中,反射自然有其用武之地。不过内省业务代码在很多时候都不是一件好事,原因有很多,一般情况下我总是建议大家不要使用反射。
  • 首先是代码可读性与工具支持。打开熟悉的IDE,寻找你的Java代码的内部依赖,很容易吧。现在,使用反射来替换掉你的代码然后再试一下,结果如何呢?如果通过反射来修改已经封装好的对象状态,那么结果将会变得更加不可控。请看看如下示例代码:
    • 比如下面Student为第三方库代码,这个时候如果别人更新setName方法名称,改为setStudentName,那么之前写的反射代码就会出现问题,而且不太好发现。
      public class Student {
      	private String name;
      	public Student() {}
      	public void setName(String name) {
      		this.name = name;
      	}
      }
      
      
      Student student = new Student();
      Method m = student.getClass().getMethod("setName", String.class);
      m.invoke(student, "张三");
  • 如果这样做就无法得到编译期的安全保证。就像上面这个示例一样,你会发现如果getDeclaredField()方法调用的参数输错了,那么只有在运行期才能发现。要知道的是,寻找运行期Bug的难度要远远超过编译期的Bug。

03.反射的弊端

  • 反射机制是一种程序自我分析的能力。用于获取一个类的类变量,构造函数,方法,修饰符。
    • 优点:运行期类型的判断,动态类加载,动态代理使用反射。
    • 缺点:性能是一个问题,反射相当于一系列解释操作,通知jvm要做的事情,性能比直接的java代码要慢很多。
  • 反射的弊端有哪些?
    • 反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。
    • 使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行。
    • 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

04.看一个案例分析

4.1 先看一个案例

  • 先看一下下面的代码
    import java.lang.reflect.Method;
    /**
     * <pre>
     *     @author yangchong
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/09/16
     *     desc  : 
     *     revise: 
     * </pre>
     */
    public class Test1 {
    	
    	public static void test1() {
    		Student student = new Student();
    		long startTime = System.currentTimeMillis();
    		for (int i = 0; i < 100000000; i++) {
    			student.setName("张三");
    		}
    		long endTime = System.currentTimeMillis();
    		System.out.printf("调用普通方法,执行1亿次,耗时%dms\n", endTime - startTime);
    	}
    
    	public static void test2() {
    		try {
    			Student student = new Student();
    			Method m = student.getClass().getMethod("setName", String.class);
    			long startTime = System.currentTimeMillis();
    			for (int i = 0; i < 100000000; i++) {
    				m.invoke(student, "张三");
    			}
    			long endTime = System.currentTimeMillis();
    			System.out.printf("调用反射方法,执行1亿次,耗时%dms\n", endTime - startTime);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    
    	public static void main(String[] args) {
    		test1(); 
    		test2(); 
    	}
    }
  • 然后看一下直接结果
    调用普通方法,执行1亿次,耗时16ms
    调用反射方法,执行1亿次,耗时172ms
  • 思考一下
    • 难道反射和普通setName相差N倍的性能吗?!!
    • 其实并不是,反射是慢,但慢多少这个例子并不能测试出来,这个例子第二个方法由于反射的存在,导致JVM无法优化。
    • 第一个例子结果出入并不大,因为JVM直接把这段代码优化了,JVM直接判定循环中的代码没有对外界造成影响,所以直接忽略调用了。

4.2 看一下第二个例子

  • 看下面的代码
    public static void test1() {
    	Student student = new Student();
    	long startTime = System.currentTimeMillis();
    	String[] arr = new String[100000000];
    	for (int i = 0; i < 100000000; i++) {
    		student.setName("张三");
    		arr[i] = student.getName();
    	}
    	long endTime = System.currentTimeMillis();
    	System.out.printf("调用普通方法,执行1亿次,耗时%dms\n", endTime - startTime);
    }
    
    public static void test2() {
    	try {
    		Class<?> cls = Student.class;
    		Student student = (Student)cls.newInstance();
    		Method m = cls.getMethod("setName", String.class);
    		long startTime = System.currentTimeMillis();
    		String[] arr = new String[100000000];
    		for (int i = 0; i < 100000000; i++) {
    			m.invoke(student, "张三");
    			arr[i] = student.getName();
    		}
    		long endTime = System.currentTimeMillis();
    
    		System.out.printf("调用反射方法,执行1亿次,耗时%dms\n", endTime - startTime);
    
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
    
    public static void main(String[] args) {
    	test1(); 
    	test2(); 
    }
  • 然后看一下执行时间
    调用普通方法,执行1亿次,耗时234ms
    调用反射方法,执行1亿次,耗时328ms
  • 思考一下

05.反射效率低原因

  • 大概有这些原因
    • Method#invoke 方法会对参数做封装和解封操作
    • 需要检查方法可见性
    • 需要校验参数
    • 反射方法难以内联
    • JIT 无法优化

06.getMethod源码

6.1 getMethod方法

  • getMethod方法和getDeclaredMethod方法如下所示
    class Class {
        @CallerSensitive
        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 getReflectionFactory().copyMethod(method);
        }
     
        @CallerSensitive
        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 getReflectionFactory().copyMethod(method);
        }
    }
  • 从上面的代码,我们可以看到,获取方法的流程分三步走:
    • 检查方法权限
    • 获取方法 Method 对象
    • 返回方法的拷贝
  • 这里主要有两个区别:
    • getMethod 中 checkMemberAccess 传入的是 Member.PUBLIC,而 getDeclaredMethod 传入的是 Member.DECLARED 这两个值有什么区别呢?我们看下代码中的注释:
      interface Member {
          /**
           * 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;
      }
    • 注释里清楚的解释了 PUBLIC 和 DECLARED 的不同,PUBLIC 会包括所有的 public 方法,包括父类的方法,而 DECLARED 会包括所有自己定义的方法,public,protected,private 都在此,但是不包括父类的方法。这也正是 getMethod 和 getDeclaredMethod 的区别。
  • getMethod 中获取方法调用的是 getMethod0,而 getDeclaredMethod 获取方法调用的是 privateGetDeclaredMethods。
    • 关于这个区别,这里简单提及一下,后面具体分析代码。privateGetDeclaredMethods 是获取类自身定义的方法,参数是 boolean publicOnly,表示是否只获取公共方法。
      private Method[] privateGetDeclaredMethods(boolean publicOnly) {
      //...
      }
    • 而 getMethod0 会递归查找父类的方法,其中会调用到 privateGetDeclaredMethods 方法。

6.2 checkMemberAccess方法

  • checkMemberAccess
    class Class {
        private void checkMemberAccess(SecurityManager sm, int which,
                                       Class<?> caller, boolean checkProxyInterfaces) {
            /* 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);
            if (which != Member.PUBLIC) {
                final ClassLoader cl = getClassLoader0();
                if (ccl != cl) {
                    sm.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
                }
            }
            this.checkPackageAccess(sm, ccl, checkProxyInterfaces);
        }
    }
    • 在这里可以看到,对于非 Member.PUBLIC 的访问,会增加一项检测,SecurityManager.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION); 这项检测需要运行时申请 RuntimePermission("accessDeclaredMembers")。这里就不继续往下看了,方法整体是在检查是否可以访问对象成员。

6.3 getMethod0方法

  • getMethod0
    class Class {
        private Method getMethod0(String name, Class<?>[] parameterTypes) {
            PublicMethods.MethodList res = getMethodsRecursive(
                name,
                parameterTypes == null ? EMPTY_CLASS_ARRAY : parameterTypes,
                /* includeStatic */ true);
            return res == null ? null : res.getMostSpecific();
        }
    }
    • 这里是通过 getMethodsRecursive 获取到 MethodList 对象,然后通过 MethodList#getMostSpecific 方法筛选出对应的方法。 MethodList#getMOstSpecific会筛选返回值类型最为具体的方法,至于为什么会有返回值的区别,后面会讲到。(这里的具体,指的是有两个方法,返回值分别是 Child 和 Parent,Child 继承自 Parent,这里会筛选出返回值为 Child 的方法)。
  • 接着看看getMethodsRecursive
    class Class {
        private PublicMethods.MethodList getMethodsRecursive(String name,
                                                             Class<?>[] parameterTypes,
                                                             boolean includeStatic) {
            // 1. 获取自己的 public 方法
            Method[] methods = privateGetDeclaredMethods(/* publicOnly */ true);
            // 2. 筛选符合条件的方法,构造 MethodList 对象
            PublicMethods.MethodList res = PublicMethods.MethodList
                .filter(methods, name, parameterTypes, includeStatic);
            // 找到方法,直接返回
            if (res != null) {
                return res;
            }
     
            // 3. 没有找到方法,就获取其父类,递归调用 getMethodsRecursive 方法
            Class<?> sc = getSuperclass();
            if (sc != null) {
                res = sc.getMethodsRecursive(name, parameterTypes, includeStatic);
            }
     
            // 4. 获取接口中对应的方法
            for (Class<?> intf : getInterfaces(/* cloneArray */ false)) {
                res = PublicMethods.MethodList.merge(
                    res, intf.getMethodsRecursive(name, parameterTypes,
                                                  /* includeStatic */ false));
            }
     
            return res;
        }
    }
  • 这里获取方法有四个步骤:
    • 通过 privateGetDeclaredMethods 获取自己所有的 public 方法
    • 通过 MethodList#filter 查找 方法名,参数相同的方法,如果找到,直接返回
    • 如果自己没有实现对应的方法,就去父类中查找对应的方法
    • 查找接口中对应的方法
  • 通过上面四个步骤,最终获取到的是一个 MethodList 对象,是一个链表结点,其 next 指向下一个结点。
    • 也就是说,这里获取到的 Method 会有多个。这里稍微解释一下,在我们平时编写 Java 代码时,同一个类是不能有方法名和方法参数都相同的方法的,而实际上,在 JVM 中,一个方法签名是和 返回值,方法名,方法参数 三者相关的。
    • 也就是说,在 JVM 中,可以存在 方法名和方法参数都相同,但是返回值不同的方法。所以这里返回的是一个方法链表。所以上面最终返回方法时会通过 MethodList#getMostSpecific 进行返回值的筛选,筛选出返回值类型最具体的方法。
  • 这里我们先暂停回顾一下整体的调用链路:
    • getMethod -> getMethod0 -> getMethodsRecursive -> privateGetDeclaredMethods
    • 通过函数调用,最终会调用到 privateGetDeclaredMethods 方法,也就是真正获取方法的地方。

6.4 privateGetDeclaredMethods

  • privateGetDeclaredMethods
    class Class {
        private Method[] privateGetDeclaredMethods(boolean publicOnly) {
            Method[] res;
            // 1. 通过缓存获取 Method[]
            ReflectionData<T> rd = reflectionData();
            if (rd != null) {
                res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
                if (res != null) return res;
            }
            // 2. 没有缓存,通过 JVM 获取
            res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
            if (rd != null) {
                if (publicOnly) {
                    rd.declaredPublicMethods = res;
                } else {
                    rd.declaredMethods = res;
                }
            }
            return res;
        }
    }
  • 在 privateGetDeclaredMethods 获取方法时,有两个步骤:
    • relectionData 通过缓存获取
    • 如果缓存没有命中的话,通过 getDeclaredMethods0 获取方法
  • 先看看 relectionData 方法:
    class Class {
        private ReflectionData<T> reflectionData() {
            SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
            int classRedefinedCount = this.classRedefinedCount;
            ReflectionData<T> rd;
            if (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;
    
        // Cached names
        String simpleName;
        String canonicalName;
        static final String NULL_SENTINEL = new String();
    
        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;
    }
    • 可以看到,保存了 Class 中的属性和方法。如果缓存为空,就会通过 getDeclaredMethods0 从 JVM 中查找方法。getDeclaredMethods0 是一个 native 方法,这里暂时先不看。
  • 通过上面几个步骤,就获取到 Method 数组。
    • 这就是 getMethod 方法的整个实现了。我们再回过头看一下 getDeclaredMethod 方法的实现,通过 privateGetDeclaredMethods 获取方法以后,会通过 searchMethods 对方法进行筛选。
      public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
          throws NoSuchMethodException, SecurityException {
          // ...
          Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
          // ...
      }
  • searchMethods 方法实现比较简单,就是对比方法名,参数,方法返回值。
    class Class {
        private static Method searchMethods(Method[] methods,
                                            String name,
                                            Class<?>[] parameterTypes)
        {
            ReflectionFactory fact = getReflectionFactory();
            Method res = null;
            for (Method m : methods) {
                // 比较方法名
                if (m.getName().equals(name)
                    // 比较方法参数
                    && arrayContentsEq(parameterTypes,
                                       fact.getExecutableSharedParameterTypes(m))
                    // 比较返回值
                    && (res == null
                        || (res.getReturnType() != m.getReturnType()
                            && res.getReturnType().isAssignableFrom(m.getReturnType()))))
                    res = m;
            }
            return res;
        }
    }

6.5 Method#copy方法

  • 在获取到对应方法以后,并不会直接返回,而是会通过 getReflectionFactory().copyMethod(method); 返回方法的一个拷贝。最终调用的是 Method#copy,我们来看看其实现。
    class Method {
        Method copy() {
            // This routine enables sharing of MethodAccessor objects
            // among Method objects which refer to the same underlying
            // method in the VM. (All of this contortion is only necessary
            // because of the "accessibility" bit in AccessibleObject,
            // which implicitly requires that new java.lang.reflect
            // objects be fabricated for each reflective call on Class
            // objects.)
            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);
            res.root = this;
            // Might as well eagerly propagate this if already present
            res.methodAccessor = methodAccessor;
            return res;
        }
    }
  • 会 new 一个 Method 实例并返回。这里有两点要注意:
    • 设置 root = this
    • 会给 Method 设置 MethodAccessor,用于后面方法调用。也就是所有的 Method 的拷贝都会使用同一份 methodAccessor。

07.invoke()源码

  • 获取到方法以后,通过 Method#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);
        }
    }
  • invoke 方法的实现,分为三步:
    • 检查权限
    • 创建 MethodAccessor
    • 调用 MethodAccessor.invoke

7.1 检查权限

  • 检查是否有权限调用方法
    • 这里对 override 变量进行判断,如果 override == true,就跳过检查 我们通常在 Method#invoke 之前,会调用 Method#setAccessible(true),就是设置 override 值为 true。
      void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
          throws IllegalAccessException
      {
          if (caller == clazz) {  // quick check
              return;             // ACCESS IS OK
          }
          Object cache = securityCheckCache;  // read volatile
          Class<?> targetClass = clazz;
          if (obj != null
              && Modifier.isProtected(modifiers)
              && ((targetClass = obj.getClass()) != clazz)) {
              // Must match a 2-list of { caller, targetClass }.
              if (cache instanceof Class[]) {
                  Class<?>[] cache2 = (Class<?>[]) cache;
                  if (cache2[1] == targetClass &&
                      cache2[0] == caller) {
                      return;     // ACCESS IS OK
                  }
                  // (Test cache[1] first since range check for [1]
                  // subsumes range check for [0].)
              }
          } else if (cache == caller) {
              // Non-protected case (or obj.class == this.clazz).
              return;             // ACCESS IS OK
          }
      
          // If no return, fall through to the slow path.
          slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
      }

7.2 获取 MethodAccessor

  • 在上面获取 Method 的时候我们讲到过,Method#copy 会给 Method 的 methodAccessor 赋值。所以这里的 methodAccessor 就是拷贝时使用的 MethodAccessor。如果 ma 为空,就去创建 MethodAccessor。
    class Method {
        private MethodAccessor acquireMethodAccessor() {
            // First check to see if one has been created yet, and take it
            // if so
            MethodAccessor tmp = null;
            if (root != null) tmp = root.getMethodAccessor();
            if (tmp != null) {
                methodAccessor = tmp;
            } else {
                // Otherwise fabricate one and propagate it up to the root
                tmp = reflectionFactory.newMethodAccessor(this);
                setMethodAccessor(tmp);
            }
     
            return tmp;
        }
    }
  • 这里会先查找 root 的 MethodAccessor,这里的 root 在上面 Method#copy 中设置过。如果还是没有找到,就去创建 MethodAccessor。
    class ReflectionFactory {
        public MethodAccessor newMethodAccessor(Method method) {
            // 其中会对 noInflation 进行赋值
            checkInitted();
            // ...
            if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
                // 生成的是 MethodAccessorImpl
                return new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            } else {
                NativeMethodAccessorImpl acc =
                    new NativeMethodAccessorImpl(method);
                DelegatingMethodAccessorImpl res =
                    new DelegatingMethodAccessorImpl(acc);
                acc.setParent(res);
                return res;
            }
        }
    }
  • 这里可以看到,一共有三种 MethodAccessor。
    • MethodAccessorImpl,NativeMethodAccessorImpl,DelegatingMethodAccessorImpl。
    • 采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会 采用 MethodAccessorImpl。
    • 所以默认会调用 NativeMethodAccessorImpl。
  • MethodAccessorImpl 是通过动态生成字节码来进行方法调用的,是 Java 版本的 MethodAccessor,字节码生成比较复杂,这里不放代码了。大家感兴趣可以看这里的 generate 方法。
  • DelegatingMethodAccessorImpl 就是单纯的代理,真正的实现还是 NativeMethodAccessorImpl。
    class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
        private MethodAccessorImpl delegate;
     
        DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
            setDelegate(delegate);
        }
     
        public Object invoke(Object obj, Object[] args)
            throws IllegalArgumentException, InvocationTargetException
        {
            return delegate.invoke(obj, args);
        }
     
        void setDelegate(MethodAccessorImpl delegate) {
            this.delegate = delegate;
        }
    }
  • NativeMethodAccessorImpl 是 Native 版本的 MethodAccessor 实现。
    class NativeMethodAccessorImpl extends MethodAccessorImpl {
        public Object invoke(Object obj, Object[] args)
            throws IllegalArgumentException, InvocationTargetException
        {
            // We can't inflate methods belonging to vm-anonymous classes because
            // that kind of class can't be referred to by name, hence can't be
            // found from the generated bytecode.
            if (++numInvocations > ReflectionFactory.inflationThreshold()
                    && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
                // Java 版本的 MethodAccessor
                MethodAccessorImpl acc = (MethodAccessorImpl)
                    new MethodAccessorGenerator().
                        generateMethod(method.getDeclaringClass(),
                                       method.getName(),
                                       method.getParameterTypes(),
                                       method.getReturnType(),
                                       method.getExceptionTypes(),
                                       method.getModifiers());
                parent.setDelegate(acc);
            }
     
            // Native 版本调用
            return invoke0(method, obj, args);
        }
     
        private static native Object invoke0(Method m, Object obj, Object[] args);
    }
  • 在 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边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

7.3 MethodAccessor#invoke

  • 调用 MethodAccessor#invoke 实现方法的调用 在生成 MethodAccessor 以后,就调用其 invoke 方法进行最终的反射调用。这里我们对 Java 版本的 MethodAccessorImpl 做个简单的分析,Native 版本暂时不做分析。在前面我们提到过 MethodAccessorImpl 是通过 MethodAccessorGenerator#generate 生成动态字节码然后动态加载到 JVM 中的。其中生成 invoke 方法字节码的是 MethodAccessorGenerator#emitInvoke。我们看其中校验参数的一小段代码:
    // Iterate through incoming actual parameters, ensuring that each
    // is compatible with the formal parameter type, and pushing the
    // actual on the operand stack (unboxing and widening if necessary).
    
    // num args of other invoke bytecodes
    for (int i = 0; i < parameterTypes.length; i++) {
        // ...
        if (isPrimitive(paramType)) {
            // Unboxing code.
            // Put parameter into temporary local variable
            // astore_3 | astore_2
            // ...
    
            // repeat for all possible widening conversions:
            //   aload_3 | aload_2
            //   instanceof <primitive boxing type>
            //   ifeq <next unboxing label>
            //   aload_3 | aload_2
            //   checkcast <primitive boxing type> // Note: this is "redundant",
            //                                     // but necessary for the verifier
            //   invokevirtual <unboxing method>
            //   <widening conversion bytecode, if necessary>
            //   goto <next parameter label>
            // <next unboxing label:> ...
            // last unboxing label:
            //   new <IllegalArgumentException>
            //   dup
            //   invokespecial <IllegalArgumentException ctor>
            //   athrow
        }
    }
  • 通过上面的注释以及字节码,我们可以看到,生成的 invoke 方法,会对传入的参数做校验,其中会涉及到 unboxing 操作。

08.总结一下分析

  • 详细一点介绍如下
    • Method#invoke 方法会对参数做封装和解封操作
      • 我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
      • 而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。
    • 需要检查方法可见性
      • 通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)
    • 需要校验参数
      • 反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);
    • 反射方法难以内联
      • Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。
    • 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.
      • 因为反射涉及到动态加载的类型,所以无法进行优化。
  • 反射的开销
    • Class.forName,调用本地方法,耗时
  • Class.getMethod,遍历该类的共有方法,匹配不到,遍历父类共有方法, 耗时,getMethod会返回得到结果的拷贝,应避免getMethods和getDeclardMethods方法,减少不必要堆空间消耗。
  • Method.invoke
    • method.invoke(null, i);将invoke的参数改变时,查看其中字节码,发现多了新建Object数据和int类型装箱的指令。原因为:
    • Method.invoke是一个变长参数方法,字节码层面它的最后一个参数是object数组,所以编译器会在方法调用处生成一个数据,传入;Object数组不能存储基本类型,所以会自动装箱
    • 这两者都会带来性能开销,也会占用堆内存,加重gc负担。但是实际上述例子并不会触发gc,因为原本的反射调用被内联(别问我啥意思,因为我暂时也不知道),其创建的对象被虚拟机认为“不会逃逸”,此时会将其优化为栈上分配(非堆上分配),不会触发GC。

参考

  • JVM是如何实现反射的?
    • https://time.geekbang.org/column/article/12192
贡献者: yangchong211
上一篇
3.4高效文件读写的原理
下一篇
4.2为何要设计注解思想