前言
我们编写的 Java
代码,通过 javac
编译为 .class
文件,称为 字节码 。字节码由 JVM
加载,运行时解释器将字节码解析为机器码执行。即时编译器针对热点代码,将对应的字节码编译为机器码,达到更高的执行效率。
JVM
加载 class
字节码的过程称为 类加载。类加载的最终产物是 堆 中 Class
对象,Class
对象封装了类在方法区内的数据结构,并向程序员提供了访问方法区内数据结构的接口。
下图显示了 Java
类的生命周期。
其中,类加载包含了 加载 (Loading
) 、链接 (Linking
) 、 初始化 (Initialization
) 三个阶段。链接阶段又包含了 验证,准备,解析 三个阶段。
类加载过程
第一阶段 加载阶段
在加载阶段,总共要做三件事:
- 通过 全类名 获取类的 二进制字节流 (包含
class
文件) - 将获取的二进制字节流所代表的静态存储结构转换为 方法区 的运行时数据结构
- 在 堆 中生成一个
java.lang.Class
对象,作为该类的访问入口
需要注意的是,获取的二进制字节流除了可以是 class
文件,还包括其他的来源:
- 从
zip
包中读取,包括JAR,WAR
等 - 网络获取
- 运行时动态生成,比如 动态代理技术
- 其他文件生成,包括 加密文件
第二阶段 链接阶段
链接阶段包含了 验证、准备、解析 三个阶段。
验证
验证的目的是 保证 Class
文件的字节流中的信息符合虚拟机的要求,确保被加载类的正确性。
此过程包含了四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
但是,真正发生在验证阶段的只有 元数据 和 字节码 验证。文件格式验证发生在加载阶段,符号引用验证发生在解析阶段。
准备
准备阶段为 static
变量 分配内存空间并设置 默认值。
由于 final
变量在 编译时就会分配内存,所以如果是 static final
修饰的变量,在准备阶段就会直接赋值。
解析
解析阶段将符号引用替换为直接引用。解析可以发生在初始化之前(静态解析),也可以发生在初始化之后(动态解析,比如多态,后期绑定)。
第三阶段 初始化
初始化阶段就是执行类的构造器方法(不是构造方法) clinit()
的过程。该方法无需定义,是 javac
编译器自动收集类中 所有类变量的赋值动作 和 静态代码块中 的语句合并而来的。
clinit()
中的指令是按照 代码的语句顺序 依次排列执行,虚拟机必须保证一个类的 clinit()
方法在多线程下同步加锁。
一个例子
public class ClinitTest {
private static int num1 = 1;
static {
num1 = 2;
System.out.println("static : " + num1); // ok
num2 = 20;
// System.out.println("static : " + num2); // error:illegal forward reference
}
private static int num2 = 10;
public static void main(String[] args) {
System.out.println("num1 = " + num1); // 2
System.out.println("num2 = " + num2); // 10
}
}
以上代码的输出结果应该是:
static : 2
num1 = 2
num2 = 10
在准备阶段,num1
和 num2
都会分配内存,并赋值为 0
。
到了初始化阶段,按照代码的顺序:
- 先将
num1
初始化为1
- 然后执行静态代码块中的内容
- 将
num1
初始化为2
, - 然后输出
static : 2
- 接着将
num2
初始化为20
- 将
- 然后再将
num2
初始化为10
- 最后执行
main
方法
由于 num2
内存再准备阶段就分配了,所以即使定义在静态代码块后面,也可以对它赋值,但是要注意,不能在静态代码块中调用 num2
。
后期绑定
在解析阶段,我们提到了如果是实现 后期绑定,那么解析会发生在初始化之后。那么什么是后期绑定呢?
后期绑定(又称动态绑定)是指在 运行时 才确定真正所调用的类型,这是实现多态的基础。
与之相对 前期绑定(静态绑定)是指在编译器就确定了真正的类型。
我们知道要发生多态,必须要重写父类的方法,所以 调用父类方法 以及 无法被重写的方法 (static,final,private
方法和构造方法)都是前期绑定,其他的情况都是后期绑定。
动态绑定只针对方法,属性都是静态绑定。
// 父类
class Pet{
String name = "pet";
public String info(){
return this.name;
}
}
// 子类
class Cat extends Pet{
String name = "cat";
@Override
public String info() {
return this.name;
}
}
public class BondTest {
public static void main(String[] args) {
Pet pet = new Cat();
System.out.println(pet.name);
System.out.println(pet.info());
}
}
以上代码输出结果为:
pet
cat
这时因为属性都是静态绑定,而重写方法是动态绑定实现的。