类加载的过程

Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段。

加载

      加载是类加载(Class Loading)过程的第一个阶段,在加载阶段,虚拟机需要完成以下3件事情:
       1、通过一个类的全限定名来获取其定义的二进制字节流。
       2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
       3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
      相对于类加载的其他阶段而言,一个非数组类的加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为既可以使用系统提供的类加载器来完成加载,也可以由自定义的类加载器来去完成加载。而对于数组类,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器仍有很密切的关系,因为数组类的元素类型最终还是要靠类加载器去创建。
      加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.Class类的对象(对HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

验证

       验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并却不会危害虚拟机自身的安全。验证阶段大致上分成4个阶段的验证动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

准备

       准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况下”是数据类型的零值,假设一个类变量的定义为:
public static int value = 123; 
那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放在类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
       但是,如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,即变量value的定义变为:
public static final int value = 123; 
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

解析

       解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

       类初始化阶段是类加载过程的最后一步,初始化阶段才真正开始执行类中定义的Java程序代码。
       准备阶段中,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的计划去初始化变量和其他资源,或者说,初始化阶段是执行类构造器<clinit>()方法的过程。
       <clinit>()方法是由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
       <clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,虚拟机执行的第一个<clinit>()方法的类肯定是java.lang.Object.
       由于父类<clinit>()方法先执行,也就意味着父类中定义的静态语句要优先于子类的变量赋值操作。
        <clinit>()方法对于类或接口来说并不是必须的,如果一个类没有静态语句块,也没有对变量赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
       接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成<clinit>()方法,但与类不同的是,接口的<clinit>()方法不需要执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。
       虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。


你可能感兴趣的:(类加载的过程)