共计 2249 个字符,预计需要花费 6 分钟才能阅读完成。
类的加载过程包含 5 个阶段:加载、验证、筹备、解析和初始化。
加载
加载的过程
“加载”是“类加载”过程的一个阶段,不能混同这两个名词。在加载阶段,虚拟机须要实现 3 件事:
- 通过类的全限定名获取改类的二进制字节流
- 将二进制字节流所代表的动态构造转化为办法区的运行时数据结构。
- 在内存中创立一个代表该类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访接口。
获取二进制字节流
对于 Class 文件,虚拟机没有指名要从哪里获取、怎么获取。除了间接编译好的.class 文件中读取,还有以下几种形式:
- 从 zip 包中读取,如 jar、war 等
- 从网络中获取,如 Applet;
- 通过动静代理技术生成代理类的二进制字节流;
- 由 JSP 文件生成对应的 Class 类
- 从数据库中读取,如有些中间件服务器能够把抉择程序安装到数据库中来实现程序代码在集群间的散发。
“非数组类”与“数组类”的加载比拟
- 非数组类加载阶段能够应用零碎提供的疏导类加载器,也能够由用户自定义的类加载器实现,开发人员能够通过定义本人的类加载器管制字节流的获取形式(如重写一个类加载器的
loadClass()
办法)。 - 数组本类自身不通过类加载器创立,它是由 Java 虚拟机间接创立的,再由类加载器创立数组中的元素类。
注意事项
- 虚拟机标准未规定 Class 对象的存储地位,对于 HotSpot 虚拟机而言,Class 对象比拟非凡,它尽管是对象,但寄存在办法区中。
- 加载阶段与连贯阶段的局部内容穿插进行,加载阶段尚未实现,连贯阶段可能曾经开始了。但这两个阶段的开始工夫依然放弃着固定的先后顺序。
验证
验证的重要性
验证阶段确保 Class 文件的字节流中蕴含的信息合乎以后虚拟机的要求,并且不会危害虚拟机本身的平安。
验证的过程
-
文件格式验证:验证字节流是否是合乎 Class 文件格式的标准,并且能被以后版本的虚拟机解决,验证点如下:
- 是否魔数 0xCAFEEBABE 结尾
- 主次版本号是否在以后虚拟机解决范畴内
- 常量池是否有不被反对的常量类型
- 指向常量的索引值是否指向了不存咋的常量
- CONSTANT_Utf8_info 型的常量是否有不合乎 UTF8 编码的数据。
- …
- 元数据验证:对字节码形容信息进行语义剖析,确保其合乎 Java 语言标准
- 字节码验证:本阶段是验证过程中最简单的一个阶段,是对办法体进行语义剖析,保障办法在运行时不会呈现危害虚拟机的事件。
- 符号援用验证:本阶段产生在解析阶段,确保解析失常执行。
筹备
筹备阶段是正式为变量(或称“动态成员变量”)分配内存并设置初始值的阶段。这些变量(不蕴含实例变量)所应用的外部都在办法区中进行调配。
初始值“通常状况下”是数据类型的零值(0,null), 假如一个类变量的定义为:
public static int value = 123;
那么变量 value 在筹备阶段过后的初始值未 0 而不是 123,因而这时候尚未开始执行任何 Java 办法。
存在“非凡状况”:如果类字段的字段属性表中存在 ConstantValue 属性,那么在筹备阶段 value 就会被初始化为 ConstantValue 竖向所指定的值,假如下面类变量 value 的定义变为:
public static final int value = 123;
那么在筹备阶段虚构机会依据 ConstantValue 的设置将 value 赋值为 123。
解析
解析阶段是虚拟机将常量池中的符号援用的过程。
初始化
类初始化阶段是类加载过程的最初一步,是执行类结构器 <clinit()>()
办法的过程。<clinit()>()
办法是由编译器主动收集类中的所有类变量的复制动作和动态语句块(static{})中的语句合并产生的,编译器收集的程序是由语句在源文件中呈现的程序锁决定的。
动态语句块中只能拜访定义在动态语句块之前的变量,定义在它之后的变量,在后面的动态语句块中能够赋值,但不能拜访,如下代码所示。
public class Test {
static {
i = 0; // 给变量赋值能够失常编译通过
System.out.println(i); // 这句编译器会提醒“非法向前援用”}
static int i = 1;
}
<clinit()>()
办法不须要显示调用父类结构器,虚构机会保障在子类的 <clinit()>()
办法执行之前,父类的 <clinit()>()
办法曾经执行结束。
因为父类的 <clinit()>()
办法先执行,意味着父类中定义的动态语句块要优先于子类的变量赋值操作。
static class Parent {
public static int A = 1;
static {A = 2;}
}
static class Sub extends Parent {public static int B = A;}
public static void main(String[] args) {System.out.println(Sub.B); // 输入 2
}
<clinit()>()
办法不是必须的,如果一个类没有动态语句块,也没有对类变量的赋值操作,那么编译器能够不为这个类生成 <clinit()>()
办法。
接口中不能应用动态代码块,但接口也须要通过 <clinit()>()
办法为接口中定义的动态成员变量显式初始化。但接口与类不同,接口的 <clinit()>()
办法不须要先执行父类的 <clinit()>()
办法,只有当父类接口中定义的变量应用时,父接口才会初始化。
虚构机会保障一个类的 <clinit()>()
办法在多线程环境中被正确加锁、同步。如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit()>()
办法。