Java虚拟机(5)-----类加载机制

        虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,就是虚拟机类加载机制。在Java语言中,类型的加载,连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性。
        虚拟机的加载过程分为下图这几个步骤:

        在具体的执行过程中,并不会严格按照上图的顺序执行,而是有可能互相穿插的。其中,解析可能会在初始化之后才会执行。
        因为没有对加载的试时机做具体的规定,所以类的加载时间相当的自由。

一.加载

        加载分为三个步骤:

           1.使用类加载通过一个类的全限定名来获取定义此类的二进制字节流并使其加载到内存当中(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等); 

             对于数组来说,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,但是数组的元素类型,最终是要靠类加载器去创建。
                              1.如果是引用类型的数组,就会使用一般的加载方式加载引用类型,并且被此数组被类加载器的类名称空间标志。
                              2.如果是基本类型的数组,就会使用引导类加载器关联,可见性默认为public

            类加载器:1.类加载器+类构成类的唯一性标志,比如instanceof   equals()方法的使用结果,不同的类加载其加载同一个类结果依然是                                            false;
                        2.分类有三:(1)启动类加载器:负责将存放在\lib目录中的,或被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别                                                                                        的类库加载到虚拟机中。如,rt.jar。名字不符合即使放在目录中也不被加载。如果需要把加                                                                                        载请求委派给引导类加载器,直接使用null代替即可。
                                           (2)扩展类加载器:由sum.misc.Launcher$ExtClassLoader实现,负责加载<Java_Home>\lib\ext目录中的,或                                                                                       者被java.ext.dir系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器。
                                   (3)应用程序类加载器:由sun.misc.Launcher$App-ClassLoader实现。是ClassLoader中的                                                                                                                          getSystemClassLoader()方法的返回值,所以也称为系统类加载器。负责加载用户路                                                                                                径(ClassPath)上所指定的类库,如果应用程序中没有自定义过自己的类加载器,这个                                                                                                 就是默认的加载器,开发人员可以直接使用这个类加载器。

                        3.双亲委派模型:


        上图即为双亲委派模型(Parents Delegation Model)。除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。父子之间的关系不以继承实现,而是使用组合关系来复用父加载器的代码。

        工作流程:如果类加载器收到类加载的请求,会先将请求委派给父类加载器完成,每一个层次的类加载器都是如此,因此所有加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈无法完成这个加载请求,子加载器才会尝试自己去加载。

        好处:如类java.lang.Object,存放在rt.jar中,无论哪个类加载器要加载这个类,都会委派到启动类加载器中,所以Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,用户自定义了一个java.lang.Object类,被加载后虚拟机中就存在多个Object类,造成混乱。

        2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,具体数据结构为明确规定;

        3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,在hotspot虚拟机中会被放在方法区中;

        加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

二.验证

    

        验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当                               前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java                           语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

       实际上,需要验证的远远不止这一点,还有很多

三.准备

        类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

    public static int value=123;
  • 1

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,这个方法会在初始化中讲到,所以把value赋值为123的动作将在初始化阶段才会执行。 至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

四.解析

           解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。

        1.符号引用(Symbolic References):即用一组符号来描述所引用的目标。它与虚拟机的内存布局无关,引用的目标不一           定已经加载到内存中

        2.直接引用(Direct References):直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。它              是和虚拟机内存布局相关的,如果有了直接引用,那引用的目标必定已经在内存中存在了。

         解析动作主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄 和 调用限定符 7类符号引用进行。

        1.类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

        2.字段解析:在对字段进行解析前,会先查看该字段所属的类或接口的符号引用是否已经解析过,没有就先对字段所属的接                            口或类进行解析。在对字段进行解析的时候,先查找本类或接口中是否有该字段,有就直接返回;否则,再对                            实现的接口进行遍历,会按照继承关系从下往上递归(也就是说,每个父接口都会走一遍)搜索各个接口和它                            的父接口,返回最近一个接口的直接引用;再对继承的父类进行遍历,会按照继承关系从下往上递归(也就是                            说,每个父类都会走一遍)搜索各个父类,返回最近一个父类的直接用。

        3.类方法解析:和字段解析搜索步骤差不多,只不过是先搜索父类,再搜索接口。

        4.接口方法解析:和类方法解析差不多,只不过接口中不会有父类,因此只需要对父接口进行搜索即可。

五.初始化

        1.初始化的时机
       有且仅有以下五种情况才会执行初始化操作
        1.使用new关键字实例化对象的时候,读取或设置一个类的静态字段(不包括被final修饰,在编译期将结果放入常量池的静态               字段)的时候,以及调用一个类的静态方法的时候

        2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,就需要先触发其初始化。

        3.初始化一个类的时候,如果父类还没有初始化,需先初始化父类。

        4.虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类.

        5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic 、REF          _putStatic、REF                 _invokeStatic的方法句柄,句柄对应的类会被初始化
        
        2.初始化的方式
           初始化是类加载过程的最后一步,此阶段才开始真正执行类中定义的Java程序代码(或者说字节码,也仅限与执行<clinit>()方法)。在准备阶段,我们已经给变量付过一次系统要求的初始值(零值)而在初始化阶段,则会根据程序员的意愿给类变量和其他资源赋值。主要是通过<clinit>()方法来执行的:
         1.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块中的语句合并产生的,编译器收集的顺            序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量定义在它之后的变            量,在前面的静态语句中可以赋值,但是不能访问

         2.<clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证             在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit             >()方法的类肯定是java.lang.Object

        3.<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编           译器可以不为这个类生成<clinit>()方法。

        4.接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()           方法。但是接口与类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中             定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

        5.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么           只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕             如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很               隐蔽的。


猜你喜欢

转载自blog.csdn.net/zh328271057/article/details/81006680