前言
反射的几个基本作用:
- 获取程序在运行时刻的内部结构。例如Constructor、Field和Method。ide 便运用了这一功能:每当我们敲入点号时,ide 便会根据点号前的内容,动态展示可以访问的字段或者方法。
- 在运行时刻对一个Java对象进行操作。
- 处理泛型
java对象在内存中的存储
当C、C++和Delphi等程序被编译成二进制程序后,原来所定义的高级数据结构都不复存在了,当Windows/Linux等操作系统(宿主机)加载这些二进制程序时,是不会加载这些语言中所定义的高级数据结构的,宿主机压根儿就不知道原来定义了哪些数据结构、哪些类,所有的数据结构都被转换为对特定内存段的偏移地址。例如C中的Struct结构体,被编译后不复存在,汇编和机器语言中没有与之对应的数据结构的概念,CPU更不知道何为结构体。C++和Delphi中的类概念被编译后也不复存在,所谓的类最终变成内存首地址。而JVM虚拟机在加载字节码程序时,会记录字节码中所定义的所有类型的原始信息(元数据),JVM知道程序中包含了哪些类,以及每个类中所关联的字段、方法、父类等信息。这是JVM虚拟机与操作系统最大的区别所在。
oop-klass模型:在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象,表示对象的实例数据。JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。
反射的实现
以下来自极客时间付费课程《深入拆解java虚拟机》的笔记。
public class Test {
public static void target(int i){
new Exception("#" + i).printStackTrace();
}
public static void main(String[] args) throws Exception {
Method method = Test.class.getMethod("target",int.class);
method.invoke(null,0);
}
}
java.lang.Exception: #0
at com.xx.xx.Test.target(Test.java:7)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.xx.xx.Test.main(Test.java:11)
从异常堆栈可以看到, method.invoke
最终还是 转换为了执行 Test.target
方法。此外, method.invoke
和 Test.target
之间还隔了
DelegatingMethodAccessorImpl.invoke
NativeMethodAccessorImpl.invoke
NativeMethodAccessorImpl.invoke0
也就是method.invoke
实际的执行过程为:
- 先执行java 代码
- 再执行 native 代码(也就是c++代码)
- 再执行java 代码(也就是
Test.target
)
整体比较耗时。
扩展:Java编译器将包含本地方法的class对应的方法添加ACC_NATIVE标识,而JVM负责将动态库加载到内存,Java执行引擎执行到本地方法时找到对应的函数,完成本地方法的调用。
为提高效率,当某个反射调用的调用次数在15(默认)以上时,便开始动态生成字节码。直接使用 invoke 指令来调用目标方法。之所以使用 委派模式DelegatingMethodAccessorImpl,便是为了能够在 NativeMethodAccessorImpl 以及 动态实现 中切换。参见下列的GenerateMethodAccessor1, 其在java 代码层面是不存在的,但在字节码层面是存在的。这样 便免掉了java==>c++==> java 的代码切换。
// GenerateMethodAccessor1 伪代码
public class GenerateMethodAccessor1 extends ...{
public Object invoke(Object obj,Object[] args)throws ...{
Test.target((int)args[0]);
return null;
}
}
反射与动态代理
2018.11.02 补充
前文提到的反射,作用有两类
- 运行时感知 类的信息
- 运行时 加载class 文件到jvm
前两者 算是信息的读取,class 信息事先便已经存在。今天再补充一个:运行时,动态提供接口实现。
Proxy.newProxyInstance
ProxyClassFactory.apply
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
如何提高反射效率
反射调用的开销
- 一些反射对象的获取比较麻烦。比如
Class.getMethod
会遍历该类的公有方法,如果没有匹配到,它还将遍历父类的公有方法。 - Method.getMethods, Method.getDeclaredMethods 会返回查找结果的一份儿拷贝(方法返回值都是数组)。
-
调用本身的开销
Method.invoke
是一个变长参数方法,在字节码层面它的最后一个参数回事Object 数据。java 编译器会在方法调用处生成一个长度为传入参数数量的Object 数组,并将传入参数传入该数组中- 由于Object 数组不能存储 基本类型, java 编译器会对传入的基本类型参数进行自动装箱
- 反射本身的委派机制
- 每次反射调用都会检查目标方法的权限。这个检查可以在java 代码里关闭
整体来说,使用反射除了带来性能开销外, 还可能占用堆内存(变长参数的分配和释放等),使得gc 更加频繁。
- 缓存得到的class/method/field/constructor对象,最好在系统启动阶段就一次性完成缓存
- 高版本jdk
setAccessible(true)
性能提高完爆前两种
The AccessibleObject class is the base class for Field, Method and
Constructor objects. It provides the ability to flag a reflected
object as suppressing default Java language access control checks
when it is used. The access checks–for public, default (package)
access, protected, and private members–are performed when Fields,
Methods or Constructors are used to set or get fields, to invoke
methods, or to create and initialize new instances of classes,
respectively. 当 Field, Method和 Constructor 这些反射对象执行的时候,默认有一个access control check,哪怕是public 方法或字段,setAccessible(true)
用于屏蔽这种检查,因而可以极大地提高效率。
小结
到这里,我们说反射能干什么?
- 动态读取class
- 动态加载class
- 动态生成class
Java Reflection - Dynamic ProxiesDynamic proxies are known to be used for at least the following purposes:
- Database Connection and Transaction Management
- Dynamic Mock Objects for Unit Testing
- Adaptation of DI Container to Custom Factory Interfaces
- AOP-like Method Interception