java的反射技术,号称是编程界的九阳神功,也可以说是框架的灵魂。也正是这种反射机制使静态语言的java具备了动态语言的某些特质。就是有了反射,才让java动态,编程的时候更加灵活。 补充:动态语言 和 静态语言 ...
java的反射技术,号称是编程界的九阳神功,也可以说是框架的灵魂。也正是这种反射机制使静态语言的java具备了动态语言的某些特质。就是有了反射,才让java动态,编程的时候更加灵活,能够动态获取信息以及动态调用对象方法。其实,Java基础技术中的代理,注解也都是依托反射才 能得以实现并应用广泛,另外我们常用的Spring、myBatis等技术框架也都是依托反射才能得以实现。
补充:动态语言 和 静态语言
(1)动态语言
动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要动态语言: Object-C.C#、JavaScript, PHP,Python, Erlang..
(2)静态语言
与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java.C.C++。
总结:Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。Java的动态性让编程的时候更加灵活。
如何理解java的反射?反射就是把java的各种成分(字段,方法)映射成相应的java类,比如:Filed类,Method类,Constructor类等,把这些类都封装到一个java类里面,这个java类就是 java.lang.Class<?>类,如下图所示:
疑问:反射机制与面向对象中的封装性是不是矛盾的?如何看待这两个技术?
不矛盾。封装性和反射是两回事。封装性主要是告诉你:没必要再去调private的东西,我有public的方法,已经把功能做的很好了。反射是:我能在运行时去解刨你这个类,并且我还有能力去调用你的private的东西。所有说不矛盾,它是两种思想,反射强调的是动态性(运行时改变程序的结构),封装强调的是保密性和可重复使用性。
扩展下:封装是指隐藏对象的属性和实现细节,仅对外提供公共的访问方式。封装的原则:1. 将不需要对外提供的东西都隐藏起来;2. 把属性都隐藏,但是要提供对应的公共访问方法,你光隐藏而不提供任何访问方法也不行啊。封装的好处:1. 提高安全性;2. 提高重用性;3.方便使用。
疑问:通过直接new的方式或反射的方式都可以调用公共的结构,用哪个更好?
答:静态写代码时,直接用new的方式比较好。程序动态运行时,用反射造对象。
要说清这个问题,我们就要从JVM的类加载以及OOP-KLASS模型说起。
在 JVM 中,使用了 OOP-KLASS 模型来表示 java 对象,即:
2.1. JVM 在加载 class 到内存时,会先解析 class 字节码资源,创建类元数据即 instanceKlass 对象 (C++ 对象) ,包括常量池、字段、方法等,存放在方法区 (hotspot 虚拟机 1.8 以后的版本对应的就是元空间, 1.8 以前的版本就是永久代) ;
2.2. 在 new 一个对象时, JVM 会创建 instanceOopDesc 实例,来表示这个对象,存放在堆区,其引用,存放在栈区或堆区;它用来表示对象的实例信息,看起来像个指针,实际上是藏在指针里的对象;instanceOopDesc对应 Java 中的对象实例;
2.3. HotSpot 并不把 instanceKlass 暴露给 Java ,而会另外创建对应的 instanceOopDesc 来表示
java.lang.Class 对象,并将 Class 对象称为 instanceKlass 对象的 “Java 镜像 ” , instanceKlass 对象有一个属性 _java_mirror 指向 Class 对象, klass 持有指向 oop 引用 ( _java_mirror 便是该 instanceKlass 对 Class 对象的引用 ) ;
2.4. 要注意, new 操作返回的 instanceOopDesc 类型有指针指向 instanceKlass ,而 instanceKlass 指向了对 应的类型的Class 实例的 instanceOopDesc ;有点绕,简单说,就是 Person 实例 —>Person 的 instanceKlass—>Person的 Class 。实际上调用 对象 .getClass() 方法时,就是通过 对象先找到
InstanceKlass 对象,然后再通过 _java_mirror 找到 Class 对象的。 instanceKlass 对象和 Class 对象都是描述 class 字节码的数据对象,只不过 instanceKlass 对象在方法区 (Hotspot实现对应是永久代或元空间,取决于 JDK 版本 ) , Class 对象在堆区, instanceKlass 对象是 C++ 对 象,Class 对象是 Java 对象 ( 当然物理上是 C++ 的 instanceOopDesc 对象,逻辑上是 Java 对象 ) 。从 Class 对象到Class 对象生成的 Constructor 对象, Method 对象 , Field 对象等,就像物体通过反射在镜子里面生成镜像一样,所以把这种方式称为反射,就很好理解了。
如下图:
综上所述,Class对象存放在方法区吗? 不是的,Class对象实例存放在堆区,存放在方法区的是c++对象,也就是 类元数据即instanceKlass对象。这个Class对象就可以理解为这个c++对象的镜像,本来这个c++对象就有这个镜像属性:_java_mirror。 这个_java_mirror就指向Class对象,其实两个是互指的,你指向我,我也指向你。
所以说我们之前总是说方法区存放了Class实例,其实这样讲不对的,Class实例是存放在堆区的,和我们new出来的java对象一样都是存放在堆区,只是字节码的 instanceKlass 对象也就是C++对象是存放在方法区的。java对象指向C++对象,C++对象再指向Class实例(二者是互指的)。
反射虽然能够使java编程更加灵活,但是它的性能怎么样呢?反射调用方法和直接调用方法这两者之间在性能上面有怎样的差距呢?先说结果吧,如下图:
先解释下这个概念哈:反射调用的时候,每次获取的Method对象,其实它都是返回的一个新对象,这就会出现一个问题,比如循环获取某一Method对象,然后再执行它。这就势必导致很多的新对象产生,这个性能是最慢的。那缓存反射调用是什么意思呢,我只获取一次Method对象,然后缓存起来,最简单的方式就是将获取的Method对象提取到循环的外边,然后循环执行这一个method对象,这个就叫缓存反射调用。
看上图得出的结论:如果不做大的优化的话,如果能缓存Method对象的话,反射耗时大约是直接调用的25倍,如果不能缓存Method对象,例如代理,AOP等场景下,那么反射耗时大约是直接调用的60倍。
编译阶段,将.java文件编译成.class文件,编译的时候都是静态的,实例类型,方法名,参数类型等都是确定的,在编译阶段编译器会做权限,可见性,参数等检验。
运行阶段,它首先是包括Java虚拟机加载类的全过程:加载,验证,准备,解析,初始化。包括这5个步骤。验证、准备、解析叫连接过程。
验证是干什么呢,就是验证文件的格式是否正确。
准备是干什么呢,准备就是给类的静态变量赋默认初始化值,注意啊,1. 静态变量,2. 赋值赋的是什么啊,是默认初始化值,它跟变量的真实值没有关系,它只跟变量的类型有关系,比如int类型,那么就赋值0,如果是Integer类型就赋值为null,boolean就赋值为false,它只跟类型有关。想一下:public static int i = 5; 那这个5是什么时候才赋值给 i 的呢? 准备阶段是默认初始化赋值的是0,那5呢,别急,后面还有个初始化阶段,这个5就是初始化阶段给赋值给 i 的。初始化就是给静态变量执行初始化,进行初始化赋值的,还有执行静态代码块,给类进行初始化。
解析是干什么呢?解析的过程会比较复杂,比如:我们的静态main方法加载进内存之后,是不是有一个对应的地址啊,解析就会将main方法这个符号标识替换成对应的内存地址的指针。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,那解析阶段中,所说的直接引用和符号引用有什么关联呢? 符号引用:符合引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,因为引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中。 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
(个人理解:解析就是将符号引用变为直接引用。为什么要这么做呢?就是要将对象加载到内存中。“直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄” 这句话意味着对象已经被加载到内存中了。因为句柄的定义就是:当前对象的唯一一个标识。这里说解析阶段的目的是什么。)
回到问题的本质,Java反射性能为什么差? 性能差是相对的,是相对于我们直接调用。
直接调用的时候,是静态的 ,实例类型,方法名,参数类型这些都是明确的,编译阶段已经处理了权限,方法可见性,参数类型等校验,之后jvm加载解析的时候已经将方法的符号引用转为地址引用了,到我们执行方法的时候,就可以直接新建栈帧进行方法调用了。
那么反射的时候就不一样了,反射是动态的,在运行的过程中才知道我们要调用什么类的什么方法,在执行的时候才明确下来,但是你那些编译阶段的校验以及一些安全机制的操作 仍然 是不能少的,所以你在执行的时候依然要做校验等安全机制的操作,所以反射性能慢。另外,它是动态的,所以可能会存在一些jvm无法优化的因素。
分析完java反射的过程以及概述了反射性能慢的原因,下面进行下原因总结:
3.1. 获取Method对象慢:a. 需要检查方法权限; b. 需要遍历筛选寻找方法,甚至还要遍历父类的方法或者接口; c, 每一个Method都有一个root,不暴露给外部,而是每次copy一Method。
3.2 调用invoke方法慢: a. invoke调用方法需要对参数做封装和解封装等操作(啥意思?invoke参数是Object,那我传int, long等基本类型的参数,它里面是不是要做封装和拆封的操作?会不会产生大量的对象?);b. 调用的时候还要检查方法的权限,还要校验参数;c, invoke调用逻辑是委托给MethodAccessor的,而这个MethodAccessor对象实懒加载,你第一次调用invoke的时候才创建。
3.3. 因为是动态加载的,vm无法做优化。
补充下:
我们获取Method这个对象,其实它返回方法的拷贝getReflectionFactory().copyMethod()方法。
里面是这样的:new 一个 Method 实例并返回。
这里有两点要注意:a. 设置 root = this; b. 会给 Method 设置 MethodAccessor,用于后面方法调用。也就是所有的 Method 的拷贝都会使用同一份 methodAccessor。
介绍下啊,不展示代码了:一共有三种 MethodAccessor。
分别是MethodAccessorImpl,NativeMethodAccessorImpl,DelegatingMethodAccessorImpl。
MethodAccessorImpl 是通过动态生成字节码来进行方法调用的,是 Java 版本的 MethodAccessor,字节码生成比较复杂,这里不放代码了。大家感兴趣可以看这里的 generate 方法。
DelegatingMethodAccessorImpl 就是单纯的代理,真正的实现还是 NativeMethodAccessorImpl。
NativeMethodAccessorImpl 是 Native 版本的 MethodAccessor 实现。
采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会 采用 MethodAccessorImpl。所以默认会调用 NativeMethodAccessorImpl。
总结下如何去优化选用吧:
1.如果反射调用场景很少,则不需要太过纠结,直接反射调用就行了。如果可以的话,我们可以将Method对象缓存起来,并且设置检查方法的可见性为true:method.setAccessible(true); 首先我们得知道这个反射的代码是没有问题的。
2.如果对性能要求较高,且无法缓存Method对象的情况下,尽量选择AsmReflect来进行反射调用。如果可以缓存,则也可以考虑使用使用Java 版 MethodAccessor,与AsmReflect差异并不是太大。
这个框架的pom依赖示例如下:
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>reflectasm</artifactId> <version>1.11.0</version>
</dependency>
本站主要用于日常笔记的记录和生活日志。本站不保证所有内容信息可靠!(大多数文章属于搬运!)如有版权问题,请联系我立即删除:“abcdsjx@126.com”。
QQ: 1164453243
邮箱: abcdsjx@126.com