前言1: 反射常用的API介绍:
获取class对象
- 调用静态方法Class.forName来获取
- 调用对象的getClass()方法
- 直接调用类名 + ".class"访问。对于基本类型而言,它们的包装类型拥有一个名为‘TYPE'的final静态字段,指向该基本类型对应的Class对象
例如,Integer.TYPE指向int.class.对于数组类型来说,可以使用类名 + "[].class"来访问,例如int[].class
一旦获取到Class对象,我们便可以正式的使用反射功能了,常用的几项:
使用newintznce()来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
使用isInstance(Object)来判断一个对象是否为该类的实例,语法上等同于instance of
使用Array.newInstance(Class,int) 来构建该类型的数组
使用getFIleds()/getConstructors()/getMethods()来访问该类的成员。方法名中带Declared的不会返回父类的成员,但是会返回私有成员,而带有Declared的则相反。
当获得了类成员之后,我们可以进一步的操作
- 使用Construct/Field/Method.setAccessible(true) 来绕开Java语言的访问限制
- 使用Construct.newInstance(Object[])来生成该类的实例
- 使用Field.get()/set(Object)来访问字段的值
- 使用Method.invoke(Object,Object[])来调用方法
前言2:内联的概念解释
方法内联指的是编译器在编译一个方法时,将某个方法调用的目标方法也纳入编译范围内,并用返回值 替代方法调用的过程。
接下来聊一聊反射
反射机制在java中的应用: ide的提示功能、java调试器,一些可配置的通用框架为了保证其可扩展性,往往借助Java的反射机制根据配置文件加载不同的类。例如Spring框架的依赖反转(ioc)便是依赖于反射机制。
但是反射机制也有它显著的缺点:性能开销大
那么反射是怎么实现的呢?看一看Method.invoke()的调用
查阅Method.invoke的源代码可以发现,它实际上会委托给MethodAccessor来处理。MethodAccessor是一个接口,它有两个具体的实现:一个通过本地方法实现反射,另一个则使用了委派模式。
每个Method实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现。本地实现非常容易理解,当进入了java虚拟机内部的时候,我们便拥有了Method实例所指向方法的具体地址,这时候反射调用无非就是将传入的参数准备好,然后调用进入目标方法。
打印反射调用的栈轨迹可以发现,反射调用先是调用了Method.invoke,然后进入委派实现(DelegatingMethodAccessorImpl),再然后进入本地方法实现(NativeMethodAccessorImpl)最后到达目标方法.
那么,为什么反射调用还要采取委派实现作为中间层?直接交给本地实现不可以吗?
其实,java反射调用机制还设立了另一种动态生成字节码的实现(动态实现),直接使用invoke指令来调用目标方法,之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。(可以联想动态代理的invoke)
动态实现与本地实现相比,运行效率要快上20倍,因为动态实现无需经过java到c++再到java的切换,但由于生成字节码十分耗时,仅调用一次的话,反而使用本地实现要快上3到4倍。
考虑到许多反射调用仅会执行一次,java虚拟机设置了一个阈值15(-Dsun.reflect.inflationThreshold= ? )来调整,当某个反射调用的调用次数在15次之下的时候,采用本地实现,当达到15时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程称之为inflation.
总结一下反射调用的性能开销
方法的反射调用会带来不少的性能开销,原因主要有三个,变长参数方法导致的Object数组,基本类型的自动装箱、拆箱还有最重要的方法内联。
以上内容是我对极客时间郑雨迪老师<<深入拆解Java虚拟机>>的精简版总结。