一、概念
程序编译后,生成class文件,经过加载、验证、准备、解析、初始化,最终使程序可以被JVM识别。
二、类的生命周期
类从被加载到虚拟机内存开始,到卸载出虚拟机内存结束,一共经历加载、验证、准备、解析、初始化、使用、卸载七个阶段。
其中验证、准备、解析统称为连接。
借用网上的图:
其中解析过程在某些情况下可以在初始化之后执行,这是为了支持JAVA的动态绑定。
三、类的初始化时机
1. 使用new实例化对象
2. 反射
3. 初始化一个类时,发现父类没有被初始化,先初始化父类
4. 指定要执行的主类包含main方法
5. 使用JDK1.7的动态语言支持时,MethodHandle实例包含REF_getputinvoke static句柄,且对应类未初始化
以上场景行为称为对类的主动引用,而被动引用不会被初始化,包含以下几个场景:
1. System.out.print(SubClass.value); // value的引用在父类SuperClass中
2. SuperClass[] sca =new SuperClass[10]; // 数组类型不会触发类的初始化
数组类型是jvm自动生成、继承Object的子类。
3.System.out.print(Constants.HELLOWORLD); // 常量类在编译时将进入常量池,本质上不会调用Constants类的初始化。
四、类加载过程
1. 加载
- 根据(包名+类名)获取该类的二进制流
- 将二进制流的静态存储结构转化为方法区的运行时存储结构
- 生成 java.lang.Class对象,并作为该类的唯一入口
2. 验证
- 文件格式验证:是否以
0xCAFEBABE
开头、class文件版本号等 - 元数据验证:语义分析,保证描述的信息符合java规范,这一步并未涉及方法体内
- 字节码验证:方法体内部代码是否合法、符合逻辑
- 符号引用验证:是否能定位到类,验证字段、类、接口方法的访问权限
3. 准备
static修饰的变量将被设置为零值。
public static int value = 123; // 在准备阶段将被赋0
public static final int value = 123; // 如果是常量,将直接初始化,而不是0,因为final的意思是:不可变的。
4. 解析
符号引用->直接引用
5. 初始化
初始化阶段才开始真正的执行JAVA代码,该阶段是虚拟机执行类构造器<clinit>()方法的过程。
<clinit>()方法有以下特点:
- 按语义顺序收集类变量的赋值动作和静态语句块static{ },其中静态语句块只能访问在它之前定义的变量,在它之后的只能赋值。
public class Test { static { i = 0; // 给变量赋值可以正常编译通过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; }
- 保证子类的<clinit>()方法在执行之前,父类的<clinit>()已经执行完成。
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。 }
- <clinit>()对于接口或类不是必须的,如果一个类没有对类变量的赋值动作,且无静态方法块static{},编译器可以
不为该类生成<clinit>()方法。
- 接口有变量赋值动作时,执行<clinit>()不会执行父类的<clinit>(),实现该接口的类初始化时不会执行接口的<clinit>方法
- 多线程下,jvm保证类的<clinit>被正确的加锁和同步,多个线程同时初始化类构造器,将只有一个类初始化,其他将
阻塞,所以<clinit>有耗时操作时将会造成线程阻塞。