类的声明周期描述了一个类加载、使用和卸载的整个过程。
一个类的声明周期包括五个阶段:加载、连接、初始化、使用、卸载,其中连接部分分为验证、准备和解析阶段。
加载阶段
- 加载阶段是第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码文件 。
- 不同的渠道比如有:本地的文件、通过动态代理生成的类、通过网络传输的类(早期 Applet 技术使用)。
- 类加载器在加载完类之后,Java 虚拟机会讲字节码中的信息保存到方法区
- 生成一个
InstanceKlass
对象,保存类的所有信息,其中还包含实现特定功能比如多态的信息。- 一个类有基本信息、常量池、字段、方法、虚方法表(多态)
- 同时,Java 虚拟机会在堆中声称一份与方法区中数据类似的
java.lang.Class
对象。可以在代码中获取类的信息以及存储静态字段的数据。
连接阶段
验证阶段
- 连接的第一个阶段是验证,验证的主要目的是检测 Java 字节码文件是否遵守了《Java 虚拟机规范》中的约束。这个阶段一般不需要程序员的参与
- 主要包含如下四种部分:
- 文件格式的验证,比如文件是否是以
0xCAFEBABE
开头的,主次版本号是否符合当前虚拟机版本的要求 - 元信息验证,例如类必须有父类(super 不能为空)
- 验证程序执行指令的语义,比如方法内的指令中跳转到不正确的位置
- 符号引用验证,比如是否访问了其他类的 private 方法等
- 文件格式的验证,比如文件是否是以
准备阶段
准备阶段为静态变量(static)分配内存并设置初始值
基本类型的初始值
但如果基本数据类型是 final
修饰的化,准备阶段会讲代码中的值进行赋值
解析
解析阶段主要是讲常量池中的符号引用替换成直接引用
符号应用就是在字节码文件中使用编号来访问常量池中的内容
转换成内存的地址来进行直接引用
初始化阶段
执行流程
初始化阶段会执行静态代码块中的代码,并且为静态变量赋值
初始化阶段会执行字节码文件中 clinit
部分的字节码指令
通过查看 clinit
中的字节码指令,可以看出在初始化阶段的执行流程:
iconst_1 // 先将 1 放到操作数栈
putstatic #2 // 将操作数栈中的值放到静态变量 valuie 中
// 静态代码块
iconst_2 // 将 2 放到操作数栈
putstatic #2 // 将操作数栈中的数放到 value 中
return
需要注意的是 clinit
中的方法的执行顺序是和 Java 中编写的顺序是一致的
类初始化的条件
- 访问一个类的静态变量或者静态方法,注意变量是 final 修饰的时候等号右边是变量不会触发初始化(final 在连接阶段的准备阶段就已经被赋值)
- 调用 Class.forName(String className)
- new 关键字来创建一个对象的时候
- 执行 Main 方法的当前类(程序的入口)
访问一个类的静态变量或者静态方法
package com.kq.init;
public class ways {
public static void main(String[] args) {
System.out.println(Demo01.i);
System.out.println(Demo02.i);
}
}
class Demo01 {
static {
System.out.println("Demo01类被初始化了");
}
public static int i = 0;
}
class Demo02 {
static {
System.out.println("Demo02类被初始化了");
}
public final static int i = 0;
}
运行结果:
Demo01类被初始化了
0
0
其他方法
package com.kq.init;
public class ways {
static {
System.out.println("主方法的类被初始化");
}
public static void main(String[] args) throws ClassNotFoundException {
// System.out.println(Demo01.i);
// System.out.println(Demo02.i);
Class<?> aClass = Class.forName("com.kq.init.Demo02");
Demo01 demo01 = new Demo01();
}
}
class Demo01 {
static {
System.out.println("Demo01类被初始化了");
}
public static int i = 0;
}
class Demo02 {
static {
System.out.println("Demo02类被初始化了");
}
public final static int i = 0;
}
运行结果:
主方法的类被初始化
Demo02类被初始化了
Demo01类被初始化了
clinit
指令在特定的情况下不会出现,比如:
- 无静态代码块且无静态变量的赋值语句
- 有静态变量的声明但是没有赋值语句
- 静态变量的定义使用 final 关键字,这类变量会在准备阶段直接进行初始化
- 在子类直接访问父类中的静态变量,不会触发子类的初始化
- 子类的初始化在 clinit 调用之前会先调用父类的 clinit 方法
在这三种情况初始化不会执行0
package com.kq.init;
public class extend {
public static void main(String[] args) {
System.out.println(Son.i);
}
}
class Father {
public static int i;
}
class Son extends Father {
static {
System.out.println("子类被初始化");
}
}
运行结果:
0