美文网首页
Java基础之反射

Java基础之反射

作者: M_JCs | 来源:发表于2017-01-05 13:59 被阅读48次

    一、两个概念

    • 编译型语言:程序在执行之前需要将源代码编译成机器语言,再由机器运行机器码(二进制)。像C/C++、Delphi等都是属于编译型语言,编译型语言需要依赖编译器,通过编译器将源代码编译成与运行平台对应的机器码,运行时不需要重新解释运行,所以程序执行效率高,但因为编译器编译的机器码与运行平台相关,所以跨平台性差。
    • 解释性语言:相对编译型语言,不需要预先编译,以文本方式存储程序代码,在运行程序时,必须先由解释器解释再运行,每执行一次就要翻译一次,效率较低,像JavaScript,VBScript,Python,Ruby等都是解释型语言。

    二、JAVA属于哪种语言?

    • 对于Java语言,Java程序首先通过编译器编译成.class文件,也就是字节码,并非是可以直接由机器直接运行的本地机器码,如果在windows平台上运行,则字节码通过windows平台上的JVM(JAVA虚拟机)进行解释执行。如果运行在linux平台上,字节码则通过linux平台上的java虚拟机进行解释执行。所以JAVA语言的跨平台特性是通过JVM的跨平台特性实现的,如果没有JVM,则不能进行跨平台。所以JAVA语言兼具解释性语言和编译型语言的特性,可以说是一种“半编译半解释”执行的语言。综合以上特性,编译型语言和解释型语言的分类就不太准确了。
    • 可以把JAVA语言划分到编译型语言中,因为编译的本质就是把一种相对高级的语言转换为另一个相对低级的语言,而由.java文件->.class文件的编译已经满足了这个特征。

    三、为什么要使用反射?

    1. 我们知道,JAVA程序需要经过编译器编译成.class文件,再由JVM解释运行,这些Class对象承载了这个类的所有信息,包括父类、接口、构造函数、方法属性等,这些.class文件在运行之前会被ClassLoader加载到虚拟机中,当一个类被虚拟机加载之后,JVM就会在内存中自动产生一个Class对象。我们通过new的形式创建对象实际上就是通过这些Class“模板”来创建实例对象,而这个过程对我们来说是透明的,具体实现细节我们不得而知。
    2. 从以上分析可知,我们编写的JAVA类需要经过编译之后再由JVM解释执行,JVM解释执行的是我们编写的JAVA类生成的对应的.class文件,因为我们无法在程序运行的过程中修改.class文件,所以,我们编写的JAVA类,到了程序真正运行的时候是无法修改的,除非我们修改JAVA类,然后在重新编译运行。那么我们怎么在程序运行的修改类的相关信息呢?这时JVM相当于给我们提供了一个接口,让我们通过反射来修改已经编译好的JAVA类。是一种以开发效率换运行效率的手段。
    3. 场景:我们需要在程序运行的过程中动态生成一个类的实例,但是我们在程序运行之前无法获知这个JAVA类的任何信息,那也就是说我们在程序中无法定义这个类,也就无法通过new方法来生成相应的实例,也无法再编译的过程生成相应的.class文件,那我们还能去动态的加载这个类并为这个类生成相应的实例吗?
    4. 反射的工作原理就是借助和反射相关的四个核心类:java.lang.Class:Class类;java.lang.reflect.Constructor:构造器类;java.lang.reflect.Method:方法类;java.lang.reflect.Field:属性类。java.lang.Class是一个Java类,继承自Object类。Class类是一个java中的泛型类型。Class类是一般类和接口的进一步抽象,而Class类的每个实例则代表运行中的一个类。Class类的构造函数是私有的,无法通过new关键字来创建Class类的实例。只能通过JVM来调用它来构造一个Class实例。在程序运行时动态访问和修改任何类的行为和状态。

    四、反射能做什么?

    1. 通过反射我们可以得到一个类的所有信息,如访问一个类中的所有属性和方法,包括访问权限受限制的属性和方法(声明为private或protected的属性和方法),通过反射我们可以访问一个对象的任意一个属性和方法。换句话说,JVM可以加载一个运行时才得知名称的.class文件,然后获悉其完整结构,并生成其对象实体、或对其fields(变量)设置,或调用其methods(方法)。
    2. 反射有如下作用:①操作因访问权限限制的属性和方法;②实现自定义注解,如依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit,Spring,Dagger。;③动态加载第三方jar包;④按需加载类。⑤实现序列化与反序列化,比如PO的ORM,Json解析等。⑥实现跨平台兼容,比如JDK中的SocketImpl的实现。

    五、反射在native中的实现

    1. Class.forName的实现
      Class.forName可以通过包名寻找Class对象,比如Class.forName("java.lang.String")。
      在JDK的源码实现中,可以发现最终调用的是native方法forName0(),它在JVM中调用的实际是findClassFromClassLoader(),原理与ClassLoader的流程一样。
    2. getDeclaredFields的实现
      在JDK源码中,可以知道class.getDeclaredFields()方法实际调用的是native方法getDeclaredFields0(),它在JVM主要实现步骤如下:
      ① 根据Class结构体信息,获取field_count与fields[]字段,这个字段早已在load过程中被放入了
      ② 根据field_count的大小分配内存、创建数组
      ③ 将数组进行forEach循环,通过fields[]中的信息依次创建Object对象
      ④ 返回数组指针
      主要慢在如下方面:1.创建、计算、分配数组对象;2.对字段进行循环赋值。
    3. Method.invoke的实现
      以下为无同步、无异常的情况下调用的步骤:
      ①创建Frame
      ②如果对象flag为native,交给native_handler进行处理
      ③在frame中执行java代码
      ④弹出Frame
      ⑤返回执行结果的指针
      主要慢在如下方面:1.需要完全执行ByteCode而缺少JIT等优化;2.检查参数非常多,这些本来可以在编译器或者加载时完成。
    4. class.newInstance的实现
      ①检测权限、预分配空间大小等参数
      ②创建Object对象,并分配空间
      ③通过Method.invoke调用构造函数(<init>())
      ④返回Object指针
      主要慢在如下方面:1.参数检查不能优化或者遗漏;2.<init>()的查表;3.Method.invoke本身耗时。

    六、反射为什么影响性能?

    1. 前面已经说到,JAVA程序在运行之前需要经过编译,然后通过ClassLoader加载到JVM中,而类加载分为:加载->验证->准备->解析->初始化五个阶段,这些都是在运行期之前完成的,反射慢就慢在把装载期做的事情搬到了运行期,也就是说在使用反射时,需要在运行程序之前把类的加载过程执行一遍。(解释正确与否不得而知)另一种说法是:编译器没法对反射相关的代码做优化。在Stackoverflow上认为反射比较慢的程序员主要有如下看法:1.验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证;2.产生很多临时对象,造成GC与计算时间消耗;3.由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一).

    相关文章

      网友评论

          本文标题:Java基础之反射

          本文链接:https://www.haomeiwen.com/subject/kkrzvttx.html