深入理解 Java 虚拟机:类的加载过程

深入理解 Java 虚拟机:类的加载过程

  • 类的生命周期
  • 加载
  • 验证
    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备
  • 解析
  • 初始化
  • 总结

类的生命周期

类从被加载到虚拟机内存中,到卸载出内存为止,整个生命周期包含:加载验证准备解析初始化使用卸载

其中 类的加载过程 包含:加载验证准备解析初始化

验证准备解析 统称为 连接

加载

类加载的三步骤:

  1. 通过一个类的 全限定名 获取定义此类的 二进制字节流
  2. 将字节流所代表的 静态存储结构 转化为 方法区的运行时数据(永久代的常量池)
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

非数组类的加载:
一个 非数组类加载过程(准确的说是加载阶段中获取二进制流的动作) 是开发人员可控性最强的,加载阶段 可使用系统提供的 引导类加载器来完成,也可以由 用户自定义加载器 去完成,开发人员可以通过 自定义类加载器 去控制 字节流 的获取方式(即重写一个类加载器的 loadClass() 方法)。

数组类的加载:
数组类 的本身并不通过 类加载器 创建,它由 Java 虚拟机直接创建。但数组类的 元素类型,最终要靠 类加载器 去创建。

验证

目的: 这一阶段是为了确保 Class 文件 的字节流中包含的信息 符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

文件格式验证

保证输入的字节流能 正确地解析并存储于方法区之内,格式上符合 Java 的信息要求,成功后字节流会被读入 方法区 中进行存储,所以后续的 3 个验证阶段都是基于方法区中的存储结构,不再操作字节流。

验证的内容:

  1. 是否以魔数 0xCAFEBABE 开头
    0xCAFEBABE(咖啡宝贝?) 是 Java 编译后 Class 文件的开头标识,有这个说明是 Java 的 Class 文件
  2. 主、次版本号是否在当前虚拟机的处理范围内
    不少人应该都遇到过,由于 JDK 版本太低,导致的 major.minor version 52.0,百度下发现要用 JDK8
  3. 常量池中的常量是否有不被支持的类型(检查常量 tag 标志)
  4. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 编码的数据
  5. Class 文件 中各个部分及文件本身是否有被删除的或附加的其他信息

元数据验证

对类的元数据进行验证,保证 不存在不符合 Java 语言规范的元数据信息

验证内容:

  1. 这个类是否有父类(除了 Object 类都应该有父类)
  2. 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)
  3. 如果这个类不是抽象类,是否实现了 父类接口 中要求实现的所有方法。
  4. 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)

字节码验证

最复杂的阶段,通过数据流和控制流的分析,确认软件语义是 合法符合逻辑 的,保证被校验类的方法 在运行时不会做出危害虚拟机安全的事件

例如:

  1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
  2. 保证指令不会跳到方法体以外的字节码指令上。
  3. 保证方法体中的类型转换是有效的。

符号引用验证

验证虚拟机将 符号引用 转化为 直接引用 时,进行的 匹配性校验

验证内容:

  1. 符号引用中通过 全限定名 是否能找到对应的类。
  2. 在指定类中是否存在 符合方法的字段描述 以及 简单名称所描述的方法和字段
  3. 符号引用的中的类、字段、方法的访问性(private、protected、public、default)是否可以被当前类访问。

准备

准备阶段 是正式为 类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

初始值为零值:
这时候进行内存分配的仅包括 类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中

其次,这里所说的初始值 “通常情况” 下是数据类型的 零值,假设一个类变量的定义为:

public static int value=123;

那变量 value准备阶段过后的初始值为 0 而不是 123
而把 value赋值为 123putstatic 指令是程序被编译后,存放于类构造器 <clinit>()方法之中,所以把value 赋值为 123 的动作将在 初始化阶段 才会执行

特殊情况:
如果类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指定的值,例如:

public static final int value=123;

编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123`。

解析

解析阶段时虚拟机将 常量池 内的 符号引用 替换为 直接引用 的过程。

符号引用与直接引用:

  • 符号引用: 以一组符号来描述所引用 目标,符号可以是 任何形式的字面量,只要使用是能 无歧义的定位到目标即可。符号引用与虚拟机内存布局无关,引用的目标 并不一定加载导内存中符号引用的字面量形式明确定义在 Class 中
  • 直接引用: 直接引用可以是直接指向目标的指针、相对偏移量或者一个能 能间接定位到目标的句柄。如果有 直接引用,那引用的目标 必定已经在内存中存在

解析动作的目标:
类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

注:解析的过程比较复杂,具体可以参考《深入理解java虚拟机》,个人认为大致了解即可

初始化

准备阶段 变量已经经过了初始值(零值)的赋值,而在 初始化阶段 将根据 程序员的主观意愿去初始化变量和其他资源。初始化阶段是执行 类构造器 () 方法 的过程。

类构造器 ():

  • () 方法是由编译器自动收集类中 所有类变量的赋值动作静态语句块 static{} 的语句合并产生的。静态语句块中 只能访问到静态语句块之前的变量之后的变量只能赋值不能访问。
public class Test {
	static {
		// 给变量赋值可以正常通过
		i = 0;
		//编译报错,提示 “非法向前引用”
		System.out.println(i);
	}
	static int i = 0;
}
  • () 方法与类的构造函数不同,它不需要显示的调用父类构造函数,虚拟机会保证子类的 () 调用之前,父类的 () 方法已经执行完毕。因此虚拟机第一个被加载的 () 肯定是 java.lang.Object
  • 如果一个类 没有静态语句块,则 不会生成 ()
  • 接口不能使用静态语句块,但是依然有 变量初始化 的赋值操作,因此也会生成 ()。但接口与类不同,接口不需要先执行父类接口的 () 方法。只有当 接口中的变量使用时,父接口才会吃实话,另外,接口实现类在初始化时也一样不会执行接口的 () 方法。
  • 虚拟机会保证一个类的 () 方法在多线程环境下是正确的加锁、同步,多个线程去初始化一个类是,只会有一个类执行 (),其他的阻塞等待

总结

深入理解 Java 虚拟机:类的加载过程_第1张图片

你可能感兴趣的:(#,《深入理解,Java,虚拟机》,第二版)