前言
前两天因为学校的项目出差,今天刚回学校.带了一本Java虚拟机在路上看,正好回来先整理一篇类加载过程.
类加载过程
类的加载过程就是指Java虚拟机将Class文件加载到内存中,对文件中的数据进行验证,准备,解析,初始化的过程.
一个类的完整的生命周期包括加载,验证,准备,解析,初始化,使用和卸载七个阶段,其中验证,准备,解析是链接部分的细分.类加载过程指的是前五个阶段.
加载阶段
加载阶段是指JVM将class字节码文件(如何确定这个文件?对应工作1)加载到内存的过程.被加载到内存的字节码文件会被存储在JVM的方法区当中(对应工作2,3),包含了当前类的静态变量,静态代码块和常量池等信息.
在加载阶段虚拟机主要完成以下的工作:
- 通过一个类的全限定类名来获取定义这个类的二进制字节流.
- 将这个字节流代表的静态结构转化为方法区的运行时数据结构.
- 在内存中生成一个java.lang.Class对象,作为方法区这个类的各类数据的访问入口.
验证阶段
验证阶段主要保证我们加载到内存中的Class文件的字节流中包含的信息符合Java虚拟机规范的约束要求,主要是为了保证安全性即我们运行代码不会对我们的虚拟机系统产生危害.
因为我们能够获取到的Class文件并不只能供通过Java源码编译获得,我们可以自己通过键盘上的0,1自己敲一个Class文件出来.
准备阶段
准备阶段主要是为类中的静态变量( s t a t i c static static关键字修饰的变量)分配内存并设置变量的初始值.
public static int value = 123; // 准备阶段 value 值为 0
public static final int finalValue = 123; // final修饰是特例 编译时会生成 ConstantValue属性
解释:
- 在准备阶段只是对类中定义的静态变量设置一个初始值(int类型是0),而不是真正将开发者设定的123赋给静态变量)
这个时候只是在给类变量赋值,而实例变量(非静态变量)会在对象实例化过程中一起分配在Java的堆当中.所以对于像
int objectValue = 123
(没有使用 s t a t i c static static关键字修饰)不在这个阶段进行讨论.
- 使用
f
i
n
a
l
final
final关键字修饰是一个特例,这样的类字段的字段属性表中会多一个
ConstantValue
属性,所以在准备阶段finalValue的值就已经被初始化为固定值123.
解析阶段
解析阶段主要是将Java虚拟机中的符号引用转换成为直接引用的过程.
初始化阶段
在初始化阶段主要是完成类变量和其他资源的初始化.可以直观理解为初始化阶段就是执行类构造器<clinit>()
方法的过程.
<clinit>()
是编译器自动收集类中的所有类变量的赋值和静态代码块(static{}
)中的语义合并而成.收集的顺序由源文件中出现的顺序决定.
// 先定义的类变量 value 后定义的静态代码块
// 所以初始化顺序就是 value -> j
class Demo{
public static int value = 123;
static{
int j = 123;
}
}
<clinit>()
方法和类的构造函数(对应<init>()
方法)不同,它不需要显示调用父类的构造器,Java虚拟机在执行子类的<clinit>()
方法前一定已经执行完毕这个类的父类的<clinit>()
方法了.
所以,在虚拟机中第一个被执行的
<clinit>()
方法的类一定是java.lang.Object.
因为它是所有类的父类!
类的加载阶段使用的是双亲委派模型.
小结
Java的类加载过程总共经历加载,验证,准备,解析,初始化五个阶段.
加载阶段主要是将Class文件加载到内存中.
验证阶段主要是校验Class文件是否符合虚拟机规范,保证所加载的文件不会损害虚拟机安全.
准备阶段主要是对静态变量赋零值.final关键字修饰的变量是个例外.
解析阶段主要是将符号引用映射成为直接引用.
初始化阶段主要是对静态变量和静态代码块中的资源进行初始化.(将静态变量真正赋值成为想要的值.)
后记
写完这部分内容终于明白为什么虚拟机先从内存区域入手了.因为内存划分是类加载的基础,我会尽快在第二弹中介绍JVM的内存模型,将这两篇文章结合起来,到时候会脉络会更加清晰.