类从被加载到虚拟机内存中,到卸载出内存为止,整个生命周期包含:加载
、验证
、准备
、解析
、初始化
、使用
、卸载
。
其中 类的加载过程 包含:加载
、验证
、准备
、解析
、初始化
验证
、准备
、解析
统称为 连接
类加载的三步骤:
非数组类的加载:
一个 非数组类
的 加载过程(准确的说是加载阶段中获取二进制流的动作) 是开发人员可控性最强的,加载阶段 可使用系统提供的 引导类加载器来完成,也可以由 用户自定义加载器 去完成,开发人员可以通过 自定义类加载器 去控制 字节流
的获取方式(即重写一个类加载器的 loadClass()
方法)。
数组类的加载:
数组类
的本身并不通过 类加载器 创建,它由 Java 虚拟机直接创建。但数组类的 元素类型,最终要靠 类加载器 去创建。
目的: 这一阶段是为了确保 Class 文件 的字节流中包含的信息 符合当前虚拟机的要求
,并且不会危害虚拟机自身的安全
保证输入的字节流能 正确地解析并存储于方法区之内,格式上符合 Java 的信息要求
,成功后字节流会被读入 方法区
中进行存储,所以后续的 3 个验证阶段都是基于方法区中的存储结构,不再操作字节流。
验证的内容:
0xCAFEBABE
开头0xCAFEBABE(咖啡宝贝?)
是 Java 编译后 Class 文件的开头标识,有这个说明是 Java 的 Class 文件major.minor version 52.0
,百度下发现要用 JDK8对类的元数据进行验证,保证 不存在不符合 Java 语言规范的元数据信息
。
验证内容:
父类
或接口
中要求实现的所有方法。最复杂的阶段,通过数据流和控制流的分析,确认软件语义是 合法
、符合逻辑
的,保证被校验类的方法 在运行时不会做出危害虚拟机安全的事件
。
例如:
验证虚拟机将 符号引用
转化为 直接引用
时,进行的 匹配性校验
。
验证内容:
全限定名
是否能找到对应的类。符合方法的字段描述
以及 简单名称所描述的方法和字段
。准备阶段
是正式为 类变量分配内存并设置类变量初始值
的阶段,这些变量所使用的内存都将在方法区中进行分配
初始值为零值:
这时候进行内存分配的仅包括 类变量(被static修饰的变量)
,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中
其次,这里所说的初始值 “通常情况” 下是数据类型的 零值
,假设一个类变量的定义为:
public static int value=123;
那变量 value
在 准备阶段过后的初始值为 0 而不是 123
而把 value
赋值为 123
的 putstatic
指令是程序被编译后,存放于类构造器 <clinit>()
方法之中,所以把value
赋值为 123
的动作将在 初始化阶段
才会执行
特殊情况:
如果类字段的字段属性表中存在 ConstantValue
属性,那在准备阶段变量 value
就会被初始化为 ConstantValue
属性所指定的值,例如:
public static final int value=123;
编译时 Javac
将会为 value
生成 ConstantValue
属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value
赋值为 123`。
解析阶段时虚拟机将 常量池
内的 符号引用
替换为 直接引用
的过程。
符号引用与直接引用:
任何形式的字面量
,只要使用是能 无歧义的定位到目标即可
。符号引用与虚拟机内存布局无关
,引用的目标 并不一定加载导内存中
,符号引用的字面量形式明确定义在 Class 中
。能间接定位到目标的句柄
。如果有 直接引用
,那引用的目标 必定已经在内存中存在
。解析动作的目标:
类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行
注:解析的过程比较复杂,具体可以参考《深入理解java虚拟机》,个人认为大致了解即可
在 准备阶段
变量已经经过了初始值(零值)的赋值,而在 初始化阶段
将根据 程序员的主观意愿去初始化变量和其他资源。初始化阶段是执行 类构造器
类构造器
public class Test {
static {
// 给变量赋值可以正常通过
i = 0;
//编译报错,提示 “非法向前引用”
System.out.println(i);
}
static int i = 0;
}
不需要
先执行父类接口的 接口中的变量使用时
,父接口才会吃实话,另外,接口实现类在初始化时也一样不会执行
接口的 正确的加锁、同步
,多个线程去初始化一个类是,只会有一个类执行
阻塞等待
。