类型与反射原理解析

276 阅读19分钟

摘要

\quad "类型与反射构成了Java世界丰富多彩的基石",作为新手的我,初看到这句话的时候,十分不以为意,难道不是那些复杂繁多根本记不出的方法名构成Java丰富多彩的嘛,他两之间好像没有联系啊。其实这两个看似孤立的知识点,了解过后发现互相之间是融会贯通的。


1.类是什么?

\quad 先回答这个问题,引用《Java 核心技术 卷I》中的话“类是构造对象的模板或者蓝图。” “由类构造对象的过程称为创建类的实例。”说白了,类就是一份对象的说明书,说明书里面描述了对象的各种信息,当我们需要一个对象的时候,就拿着这份说明书去创建一个对象。
\quad 首先我们知道,在创建好一个class对象之后:

public class Animal {
}

我们可以在对应的目录下,看到一个.java文件也就是常说的Java源文件:

源文件里面存着我们的Java代码,但是这里面的代码,JVM是不认识的,他认识的是字节码文件。这就需要编译过程了,当我们点击运行按钮之后,在target目录下,我们可以看到程序为我们自动生成了.class文件。

这个就是所谓的Java字节码文件,字节码中的内容可以使用ASM Bytecode Viewer插件查看字节码文件的内容:

(ps:看字节码可以有助于加强自己对程序执行过程的理解,也可以发现一些彩蛋,例如泛型那里有个运行时擦除,就可以通过看字节码发现。)
\quad 那么只要拿着这个.class文件,JVM就可以为我们创建好对应的对象。那么问题来了,我们创建好一个对象后,不可避免的要把这个对象传给其他对象,那么如此往复,对象会不会迷失自己的类型呢?这里就引出了一个概念了,叫RTTI-(Run Time Type Identification) 运行时类型识别,既任何一个对象在任意时刻都知道自己的是什么类型的。引用《Java编程思想》中的话"这个RTTI正是由Java的Class对象来执行的,即使你正在执行的是类似转型这样的操作


2.类是如何被加载的?

\quad 前面提到过,.java文件在编译过后被转换成了.class文件,这个文件可以被JVM识别,那么问题来了,JVM识别并创建对象的过程是什么呢?这个问题,我在《深入理解Java虚拟机》中找到了答案。
\quad 祭出图片:

注意:
\quad 加载、验证、准备、初始化和卸载的顺序是固定的,而解析阶段则不一定,在某些情况下可以在初始化阶段过后再开始。


\quad 详细解释(以下内容均来自《深入理解Java虚拟机》,学艺不精,只得照搬)。

2.1加载

\quad 首先注意区分"类加载"与"加载",加载是类加载的一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  • 1.通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  • 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

问题来了,字节流文件是否一定要来自.class文件?自己如何实现一个类加载器?
答案:
\quad虚拟机规范并没有指定二进制字节流需要从一个Class文件中获取,准确的说是没有指明要从哪里获取、怎么获取。所以在加载这个阶段,可以由开发人员自行决定字节流文件的来源。例如:

  • 1.从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 2.从网络中获取,这种场景最典型的就是Applet。
  • 3.运行时由计算机生成,这种场景使用得最多的就是动态代理技术。
  • 4.由其他文件生成,典型场景就是JSP应用,即由JSP文件生成对应CLass类。
  • 5.从数据库中读取,这种场景应用的相对较少。

\quad拿到这个Class文件之后,把字节流文件转换为对象的过程,就是类加载器的事情了。那么这个阶段也可以由开发人员灵活操控。因为加载阶段既可以使用系统提供的引导类加载器去完成,也可以由用户自定义的类加载器去完成,开发人员可以通过自定义的类加载器去控制字节流的获取方法(继承ClassLoader,并且重写findClass()方法即可),这个过程网上很多例子,在这就不做说明了。
再问个问题:数组类的加载与其他类的加载有何不同?
答案:
\quad对数组而言,数组类本身不通过类加载器加载,他是由Java虚拟机直接创建的。但数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终要靠类加载器去创建,一个数组类的创建过程遵循以下原则:

  • 1.如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型) 是引用类型,那么就递归采用现在讲到的加载过程去加载组件类型。该数组将在加载该组件类型的类加载器的类名空间上被标识(这点很重要,一个类必须与类加载器一起确定唯一性)
  • 2.如果数组的组件类型不是引用类型,Java虚拟机将会把数组标记为引导类加载器关联。
  • 3.数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

\quad类加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。
问:加载阶段与连接阶段是按照严格的先后顺序来的嘛?
答案:
\quad 加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中的动作,任然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。


2.2验证

\quad 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
\quad 验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。如果验证到输入的字节流不符合Class文件的约束,虚拟机就抛出java.lang.VerifyError异常或者其子类异常。

\quad 验证阶段大致分为下面四个阶段的检验动作:文件格式检验、元数据验证、字节码验证、符号引用验证。

  • 1.文件格式验证
  • \quad这一阶段验证的是字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。验证点包括:

      • 1.1 是否以魔术0xCAFEBABE开头。

    \quad 常用的就是用Nodepad++查看文件的二进制字节流了。下载完毕后,导入HEX Editor插件,注意官网下载下来的好像是32位系统使用的,64位系统的我找到一个64位HEX Editor,亲测可用。

      • 1.2 主、次版本号是否在当前虚拟机处理范围之内。
      • 1.3 常量池的常量中是否有不被支持的常量类型。
      • 1.4 指向常量的各种索引值中是否有指向不存在或者不符合类型的常量。
      • 1.5 CONSTANT_Utf8_info 型的常量中是否有不符合UTF8编码的数据。
      • 1.6 Class中各个部分及文件本身是否有被删除的或附加的其他信息。
        ......远不止列出的这几条。

    问:文件格式验证的目的是什么?
    回答:
    \quad 该阶段验证的主要目的是保证输入的字节流能正常地解析并存储于方法区中。只有通过了这个阶段的验证,字节流才会进入内存的方法区中进行存储。所以,后面的验证就不会直接操作字节流了,而是基于方法区的存储结构进行的。

    • 2.元数据验证

    \quad 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

      • 2.1 这个类是否有父类(除Object之外,其他类都应该有父类)。
      • 2.2 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
      • 2.3 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
      • 2.4 类中的字段、方法是否与父类产生矛盾。
        ......

    问:元数据验证的目的是什么?
    回答:
    \quad 第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

    • 3.字节码验证

    \quad 第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。检验的内容包括:

      • 3.1 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
      • 3.2 保证跳转指令不会跳转到方法体以外的字节码指令上。
      • 3.3 保证方法体中的类型转换是有效的。
        ......

    问:文件通过了字节码验证,能说明文件是安全的嘛?
    回答:
    \quad 如果一个类方法体的字节码没通过字节码验证,那么肯定是有问题的。但是通过也也不能说明其一定是安全的,原因在于:通过程序去校验程序逻辑是无法做到绝对准确的。(ps:感觉像哲学问题。)

    • 4.符号引用验证

    \quad 最后一个阶段的检验发生在虚拟机将符号引用转换为直接引用的时候,这个动作可以在第三验证阶段中发生。符号引用可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。通常需要校验下列内容:

      • 4.1 符号引用中通过字符串描述的全限定类名是否能找到对应的类。
      • 4.2 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
      • 4.3 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问。
        ......

    问:就总体而言,验证阶段是必须的嘛?
    回答:
    \quad 对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。如果运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复的使用和验证过,那么为了缩短虚拟机类加载的时间,可以在实施阶段使用-Xverify:none参数来关闭大部分的类验证措施。


    2.3准备

    \quad 准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。注意:
    \quad 1.这时候进行内存分配的只有类变量,而不包括实例变量。实例变量将会在对象实例化后随着对象一起分配在Java堆中。
    \quad 2.这里的"初始值"在"通常情况下"是数据的"零值"。例如:

    public static int i = 1;
    

    \quad 那么变量i在准备阶段过后的初始值为0而不是1(Amazing!),因为这时候尚未开始执行任何Java方法,而把i赋值为1的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法(init与clint的区别)中,所以把value赋值为1的动作在初始化阶段才会执行。

    表2-1基本数据类型的零值
    数据类型 零值 数据类型 零值
    int 0 boolean false
    long 0L float 0.0f
    short (short)0 double 0.0.d
    char '\u0000' byte (byte)0

    那么问题来了:带有final标识且赋予处置的静态变量,例如: public static final int i = 1; 在准备结果过后,会被赋予初值还是零值?
    答案:
    \quad 编译时会Javac会为i生成ConstantValue属性(带有static final属性的才有),那么在准备阶段变量i就会被初始化为ConstantValue属性所指定的值。


    2.4解析

    \quad 解析的过程是在是过于复杂,感觉一时半会讲不清楚,这里放出一些重要的概念,面试或者考试考到,也好说个大概。
    \quad 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析过程包括类或接口的解析、字段解析、类方法解析、接口方法解析。

    • 符号引用——符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时能无歧义的定位到目标即可。
    • 直接引用——直接引用可以使指向目标的指针、相对偏移量或者是一个能间接定位到对象的句柄。

    2.5初始化

    \quad 初始化阶段是执行类构造器<clinit>()方法的过程。到了初始化阶段才开始真正执行类中定义的Java代码。
    接下来就介绍一下<clinit>()方法:

    • <clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,语句在源文件出现的顺序,决定了被编译器收集的顺序。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,可以在他之前的静态语句块中赋值,但是不能访问。 举个书上的例子:
    • <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法不同),他不需要显示的调用父类的构造器,虚拟机会保证父类的<init>()方法在子类之前完成。所以,虚拟机首先执行的定是java.lang.Object构造方法。
    • 当接口或类中没有对变量的赋值操作时,<clinit>()方法则不是必要的。
    • 接口的<clinit>()方法与类不同,如果不使用父接口中定义的变量,那么子接口的初始化不会对父接口进行初始化。
    • <clinit>()方法是线程安全的。

    2.6类加载器

    这一部分的内容,书上讲的还是比较多的,在这就写出两个需要记住的知识点。 \quad 实现通过一个类的全限定类名来获取描述此类的二进制字节流的代码块,我们称之为“类加载器”。
    注意:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才会有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类必定不相同。
    \quad 双亲委派加载模型——如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。源代码可以自己去查看在ClassLoader抽象类中的loadClass()方法。

    \quad 到这,一个类的加载过程就讲完了。就我个人而言,感觉看完这个类的加载过程之后,我对反射的一些机制就了解的更加清楚了。例如,如果我们知道一个类中的内容,也就是拿到了.class文件,那么我们就可以引用类中的方法、变量等。那要是事先没有这个.class文件呢?前面提到,类加载的字节码文件可以运行时由计算机生成,由Java反射机制在运行期间进行.class文件的获取及检查。所以接下来介绍反射机制。


    3.反射:运行时的类信息

    \quad 前面说到的RTTI可以告诉我们对象在任意时刻的确切类型,但是前提是这个类在编译期间就是已知的。但是当我在编译之前不知道这个类的信息,而是通过运行时从其他地方获取获取信息的时候,并且我们还想调用类里面的变量、方法、构造器等获取类中的内容的时候,反射就体现出了他的灵活性了。举例说明:

    • 1.根据传入的参数动态创建对象
      public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            String objectClass = args[0];
            //根据程序运行时的参数,获取Class说明书
            Class klass = Class.forName(objectClass);
            //用这份说明书去创造一个实例
            Object object = klass.getConstructor().newInstance();
            System.out.println(object.getClass());
        }
    

    在Program arguments中写入:java.lang.String,

    运行结果:

    class java.lang.String
    
    Process finished with exit code 0
    
    • 2.根据传入的方法名,动态调用方法
    public class Animal {
        public String head = "head";
        public String leg = "leg";
        public static void canRun(String name) {
            System.out.println("I'm Running");
        }
    }
    
    public class Cat extends Animal {
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            Cat cat = new Cat();
            //拿到对应名称的方法,并调用它
            cat.getClass().getMethod(args[0],String.class).invoke(cat,"");
        }
    }
    
    

    在Program arguments中写入:canRun

    结果如下:

    I'm Running
    
    Process finished with exit code 0
    
    • 3.获取类中的参数
    public static void main(String[] args) throws IllegalAccessException,NoSuchFieldException {
            Cat cat = new Cat();
            System.out.println(cat.getClass().getField(args[0]).get(cat));
        }
    

    结果如下:

    head
    
    Process finished with exit code 0
    
    • 4.注意区分getMethod与getDeclearMethod
      直接看源代码:

    二者检查的访问权限主要区别还在这里,再到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;
    
    

    答案就很明了了:
    \quad getMethod是只获取公有方法及接口,包括继承的公有方法及接口,getDeclaredMethod会获取自己所有的方法及接口,但是不包括继承的。 getField与getDeclaredField的区别如出一辙。



    扩展说明:
    1.Jar、War、Ear文件的区别:

    \quad Jar文件(扩展名为.Java Application Archive)包含Java类的普通库、资源(resources)、辅助文件(auxiliary files)等。
    \quad War文件(扩展名为.Web Application Archive)包含全部Web应用程序。在这种情形下,一个Web应用程序被定义为单独的一组文件、类和资源,用户可以对jar文件进行封装,并把它作为小型服务程序(servlet)来访问。
    \quad Ear文件(扩展名为.Enterprise Application Archive)包含全部企业应用程序。在这种情形下,一个企业应用程序被定义为多个jar文件、资源、类和Web应用程序的集合。
    2.init与clinit的区别:
    \quad init()-是instance实例构造器,在遇到new指令时,对非静态变量进行初始化。
    \quad clinit()-是类构造器在加载class的时候,jvm调用的方法,用于对静态块、静态变量的初始化。


    4.总结

    \quad 这篇文章前前后后花了近3天的时间才写完,一是自己确实刚开始的时候不理解什么是反射,觉得死记方法就是,所以理解起来有点困难。再一个,一直不知道怎么开头,就拿类加载的过程来说,不看书也不知道还有这么一个过程。虽然花的时间多,而且大部分内容都是书上的,但是我感觉这一趟下来,自己收获颇丰。在把书上的内容搬到文章中的过程中,这些知识相当于都过了一边脑子的,并且书上说的很复杂,你会想着去摘取句子主干,自然会自己去理解书中内容。而不是像平时看书一样,看一遍觉得会了,上个厕所的功夫就忘了。
    \quad 如有笔误或逻辑上的错误,欢迎指正,谢谢!


    5.参考资料:

    1.《深入理解Java虚拟机》[第二版] 周志明 著——北京.机械工业出版社.2019.05

    2.《Java编程思想》[第四版] (美)Bruce Eckel 著;陈浩鹏译.——北京:机械工业出版社.2007.6(2018.9重印)

    3.《Java核心技术 卷I》[基础知识](美)凯 S.霍斯特曼(Cay S.Horstmann)著;周立新 陈波 叶乃文等译.——北京:机械工业出版社.2018.5

    4.《java类加载器》 点击此处跳转至原文

    5.《JAR、WAR、EAR的使用与区别》点击此处跳转至原文

    6.《深入理解jvm--Java中init和clinit区别完全解析》点击此处跳转至原文

    7.《大家都说 Java 反射效率低,你知道原因在哪里么》点击此处跳转至原文

    8.《Java Reflection(反射机制)详解》点击此处跳转至原文