JVM———类加载机制(二)

3.类加载的全过程

java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。

1.加载

加载阶段,虚拟机需要完成以下3件事情:

①通过一个类的全限定名来获取此类的二进制流。——开发人员可控性最强的阶段(常见从jar包中读取)

②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个累的各种数据的访问入口。——这是一个特殊的对象,它存放在方法区中

这里对加载的第一阶段还需要进行更加详细的描述:

相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的阶段)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。开完人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。

对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由java虚拟机直接创建的。但是数组类与类加载器仍然有密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终是要靠类加载器去创建,一个数组类创建的过程就遵循以下规则:

①如果数组的组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标志(这点很重要,之后会介绍到,一个类必须与类加载器一起确定唯一性)。

②如果数组的组件类型不是引用类型(例如int[]数组),java虚拟机将会把数组c标记为与引导类加载器关联。

③数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组的可见性将默认为public。

加载阶段还未结束,连接阶段可能已经开始了,虽然两个阶段可能交叉进行,但是两个阶段的开始时间仍然保持着固定的先后顺序

2.验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。

为什么有验证阶段?

Java语言本身是相对安全的语言(依然是相对于C/C++来说),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并为实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。

但是前面也说过Class文件并不一定从Java源码编译而来,甚至可以直接编写Class文件。在字节码层面上,上述Java语言无法做到的事都是可以实现的,因此必须有验证阶段,防止输入有害字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

如果验证到输入的字节流不符合Class文件格式的越是,虚拟机就应抛出一个java.lang.VerifyError异常或其子类异常。

验证阶段大致上会完成下面4个阶段的校验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

主要目的是保证输入的字节流能正确的解析并存储与方法区内,格式上符合描述一个Java类型信息的要求。只有通过了这个阶段的验证后,字节流才会进入内存的方法区内进行存储,所以后面的3个阶段都是基于方法区的存储结构进行的,不会再直接操作字节流,

元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据中的数据类型做完校验之后,这个阶段将对累的方法体进行校验分析,保证北郊眼泪的方法在运行时不会做出危害虚拟机安全的事件。

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

目的是确保解析动作能够正确执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibloeClassChangeError异常的子类。

3.准备

准备阶段是正式为类变量分配内存设置类变量初始值(零值)的阶段。这些变量所使用内存都将在方法区中进行分配。

两点需要强调:①这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。②这里所说的初始值“通常情况”下是数据类型的零值。相对也有一些是特殊情况,如果类字段的字段属性表存在ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,如代码

public static final int value=123;

因为有了final,所以编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue将value赋值为123。

4.解析

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

符号引用(Symbolic Reference):符号引用可以使任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中。

直接引用(Direct Reference):直接引用可以使直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般会不同。如果有了直接引用,那引用的目标必定已经在内存中存在。

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

5.初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说字节码)。

初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成的。

需要注意的是,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

public class Test{
    static{
        i=0;                  //给变量赋值可以正常编译通过
        System.out.print(i);  //这句编译器会提示“非法向前引用”
    }
    static int i=1;
}

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

③由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

④<clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态与太快,也没有对变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。

⑤接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,需要注意的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时,也一样不会执行接口的<clinit>()方法。

⑥虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。

猜你喜欢

转载自my.oschina.net/u/3786691/blog/1807257