Java虚拟机中类加载的过程包括加载、验证、准备、解析和初始化这五个阶段
一、加载
此阶段虚拟机完成3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
其中,获取二进制字节流不一定是从Class文件中获取,例如:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
- 从数据库中读取,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库来完成程序代码在集群间的分发
二、验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
三、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里有两点需要注意一下
- 此时进行内存分配的仅包括类变量(被static修饰),不包括实例变量
- 初始值“通常情况”下是数据类型的零值
public static int value = 123;//变量value在准备阶段过后的初始值是0而不是123
特殊情况,如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0
public static final int value = 123;//变量value在准备阶段过后的初始值是123
表1 不同数据类型所对应的零值
数据类型 | 零值 |
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
四、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。
- 符号引用:以一组符号来描述所引用的目标
- 直接引用:可以是直接指向目标的指针、相对偏移量或是能间接定位到目标的句柄
五、初始化
类初始化阶段是类加载过程中的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋值过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。初始化阶段是执行类构造器<clinit>()方法的过程
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态句句快可以赋值,但是不能访问。
class Test{
static {
i = 0;
System.out.println(i);
}
static int i = 1;
}
public class Demo{
public static void main(String[] args){
Test test = new Test();
}
}
运行结果
- <clinit>()方法与类的构造函数不同,不需要显示地调用父类构造器,虚拟机会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
class Father{
public static int A = 1;
static{
A = 2;
}
}
class Son extends Father{
public static int B = A;
}
public class Demo{
public static void main(String[] args){
System.out.println(Son.B);
}
}
运行结果
- <clinit>()方法对于类和接口并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
- 执行接口中的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时,才会初始化。
- 虚拟机会保证一个类的<clinit>()方法再多线程环境中被正确地加锁、同步。
关于类在什么情况下会初始化,可以参考这篇文章