Java虚拟机中类加载的全过程,分为加载、验证、准备、解析和初始化这5个阶段。
加载
在加载阶段,虚拟机需要完成3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
一个非数组类的加载需要借助类加载器,可以是系统的引导类加载器,也可以是自定义的类加载器。数组类的加载本身不通过类加载器(系统的引导类加载器)创建,它是由Java虚拟机直接创建,但数组类的元素类型最终还是要靠类加载器去创建。
验证
虽然Java本身相对安全,但是Class文件是可以通过任何渠道产生的,因此为了不破坏Java虚拟机的安全,要进行验证(可以用户关闭,因为不影响运行期)。分为四个阶段:
- 文件格式验证:字节流是否符合Class文件格式的规范。只有通过了这个阶段的验证,字节流才会进入内存的方法区进行存储。所以后面的3个阶段全是基于方法区的数据结构进行,不直接操作字节流。
- 元数据验证:对字节码的描述的信息进行语义分析,保证其符合Java语言规范。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个动作发生在连接的第三阶段——解析,符合引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配(注意:是方法区,而不是Java堆)。
首先,这时候进行内存分配的仅仅包括类变量(static修饰的变量),不包括实例变量,实例变量随着对象实例化一起分配在Java堆中。其次,这里的初始值通常是数据类型的零值,如public static int value=123;
,那么变量在准备阶段后的初始值为0而不是123,因为这时还未执行任何Java方法,而把value值赋值为123的动作存放于类构造器<clinit>
方法中,在初始化阶段才会执行。
但是有一种例外,public static final int value=123;
在这条语句中,编译时Javac会把value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性(此属性只对static的final变量起作用)将value赋值为123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用和直接引用的差别
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。选4种比较重要的详细讲解:
类或接口的解析
假设当前代码所处的类为D,要把一个从未解析过的符号引用N解析为一个类或接口的直接引用,虚拟机要完成以下三个步骤:
字段解析
首先解析字段所属的类或接口的符号引用。如果这个过程发现异常,则导致字段符号引用解析的失败。如果成功,那将这个字段所属的类或接口用C表示,虚拟机规范要求按如下步骤对C进行后续字段的搜索:
类方法解析
与字段解析一样,要先解析出方法所属的类或接口的符号引用,如果解析成功,用C来表示,执行下面的步骤:
接口方法解析
先解析方法所属类或接口的符号引用,成功后执行下面步骤:
由于接口中的所有的方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析不会抛出java.lang.IllegalAccessError异常。
初始化
类初始化是类加载的最后一步,在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与,其余过程都由虚拟机主导和控制。而到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的计划去初始化类变量和其他资源。或者可以这么说:初始化阶段是执行类构造器<clinit>()
方法的过程。
下面是类构造器可能会影响程序运行行为的特点和细节: