类加载步骤
jvm
将类的加载过程分为三个步骤:装载
(Load)、链接
(Link)和初始化
(Initialize)。
类生命周期
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)
、验证(Verification)
、准备(Preparation)
、解析(Resolution)
、初始化(Initiallization)
、使用(Using)
和卸载(Unloading)
这7个阶段。其中验证、准备、解析
3个部分统称为连接(Linking)
,这七个阶段的发生顺序如下图:
图中,加载、验证、准备、初始化、卸载
这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析
阶段不一定:它在某些情况下可以初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过程。使用没什么好说的,卸载属于GC的工作 。
1.装载
装载是类加载的第一个阶段。有两种时机会触发类装载:
1)预加载
虚拟机启动时装载,装载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起装载。要证明这一点很简单,写一个空的main函数,设置虚拟机参数为"-XX:+TraceClassLoading"来获取类装载信息,运行一下:
2)运行时装载
虚拟机在用到一个A.class文件的时候,会先去内存中查看一下这个A.class文件有没有被装载,如果没有就会按照类的全限定名来装载这个类。
装载阶段做三件事:
- 通过一个类的全限定类名获取类的二进制字节流
- 将在这个字节流代表的静态存储结构转化为方法区的运行时内存
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
注意:二进制字节流来源可能有如下:
1.从zip包中获取,这就是以后jar、ear、war格式的基础
2.从网络中获取,典型应用就是Applet
3.运行时计算生成,典型应用就是动态代理技术
4.由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
5.从数据库中读取,这种场景比较少见
2.链接
就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含以下步骤:
1. 验证:确保被加载类的正确性
为什么要做验证?因为class文件未必要从Java源码编译而来,可以使用任何途径产生。
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃。
检验被加载的类是否有正确的内部结构,并和其他类协调一致。
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
- 文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
- 符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
2. 准备:为类的静态变量分配内存,并将其赋默认值
- 为类的static修饰的静态属性分配内存,并设置默认初始值(即零值,如0、0L、null、false等);
- 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
各个数据类型的零值如下表:
3.解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。
假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。
例子:
public class TestMain {
private static int i;
private double d;
public static void print() {
}
private boolean trueOrFalse(){
return false;
}
}
这段代码的.class反编译一下:
3.初始化
为类的静态变量赋初值。赋初值两种方式:
- 定义静态变量时指定初始值。如 private static String x=“123”;
- 在静态代码块里为静态变量赋值。如 static{ x=“123”; }
注意:只有对类的主动使用才会导致类的初始化。
类初始化方法clinit 与 实例初始化方法init
类初始化方法clinit
clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。
注意事项
:
1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
2. 在执行clinit方法时,必须先执行父类的clinit方法。
3. clinit方法只执行一次。
4. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。
实例初始化方法init
init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
注意事项
:
1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。
2. 在执行init方法时,必须先执行父类的init方法。
3. init方法每实例化一次就会执行一次。
4. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。