[Java]|不能不了解的,类加载机制


文章目录
    • 1. 类文件
    • 2. 类的生命周期
    • 3. 类加载
      • 3.1 加载
      • 3.2 验证
      • 3.3 准备
      • 3.4 解析
      • 3.5 初始化
    • 4. 类加载器
      • 4.1 JVM 角度区分两种类加载器
      • 4.2 Java 程序中系统提供的三种类加载器
      • 4.3 类加载器的双亲委派模型
    • 5. 类文件结构(扩展阅读)
      • 5.1 魔数与文件版本
      • 5.2 常量池
      • 5.3 访问标志
      • 5.4 类索引、父类索引与接口索引集合
      • 5.5 字段表集合、方法表集合、属性表集合

1. 类文件 类文件是以 8 位字节为基础单位的二进制字节流
[Java]|不能不了解的,类加载机制
文章图片

图1-1、类文件字节码演示图 2. 类的生命周期 加载-验证-准备-解析-初始化-使用-卸载
某些情况下,解析可以在初始化之后;比如程序支持动态绑定。
[Java]|不能不了解的,类加载机制
文章图片

图2-1、类的生命周期图 3. 类加载 加载阶段-验证阶段-准备阶段-解析阶段-初始化阶段
加载阶段主要是使用类加载器,根据类的全限定名,获取类的二进制字节流;
验证阶段对文本格式、元数据、字节码、符号引用进行验证;
准备阶段在非堆为静态变量分配内存,设置初始值零值;
解析阶段将常量池内的符号引用替换为直接引用;
初始化阶段开始执行字节码,初始化类变量和其他资源,对静态变量进行赋值等。
3.1 加载
加载只是类加载的第一个阶段,类加载全过程包括加载、验证、准备、解析、初始化。
  • 设计
    通过类的全限定名获取定义此类的二进制字节流,将字节流代表的静态存储结构转化为运行时数据结构,在内存中生成 java.lang.Class 对象
通过类全限定名获取二进制字节流的方式可以是
(1) 从 jar、war、war、zip 包读取
(2) 从网络读取
(3) 动态代理
(4) 由其他文件生成
(5) 从数据库读取
3.2 验证
验证是类加载阶段重要非必须阶段,可以通过 -Xverify:none 关闭这一耗时措施,来缩短类加载时长。
  • 设计
    验证类文件的字节流中信息符合当前 JVM 要求,确保对当前 JVM 是安全的
  • 实现
    文本格式验证,元数据验证、字节码验证、符号引用验证。
(1) 文件格式验证
验证字节流是否符合类文件格式的规范,确保都能被当前 JVM 处理
(2) 元数据验证
对类的元数据信息进行语义校验,确保都符合 Java 语言规范
(3) 字节码验证
验证类的方法体,通过数据流和控制流分析,验证类方法体的字节码,确保程序语义合法且符合逻辑;
(4) 符号引用验证
确保解析动作能正常执行
类型检查:
方法体 Code 属性的属性表增加名称为 StackMapTable 的属性,描述方法体中所有基本块开始时的本地变量表和操作栈应有的状态,字节码验证期间可以通过校验 StackMapTable 属性中的记录是否合法即可。
数据流验证复杂,JDK6和JDK7 实现了类型检查,JDK7 之后对于主版本号大于 50 的类文件,使用类型检查完成数据流分析校验。
3.3 准备
  • 设计
    准备是在非堆为类的静态变量分配内存,设置静态变量初始值
  • 示例及说明
public static int valuie = 123;

一般的,在准备阶段,静态变量在非堆分配内存,准备阶段静态变量初始值为零值。初始化阶段会赋值 123 给 value。
public static final int valuie = 1;

特殊情况,final 修饰的静态变量,编译时会为 value 生成 ConstantValue 属性,在准备阶段就会根据 ConstantValue 对 value 赋值为 1。
八大基础数据类型零值为 (boolean)false,(byte)0,(char)’\u0000’,(short)0,(int)0,(float)0.0f,(long)0,(double)0.0d;引用类型零值为 (reference)null;
3.4 解析
解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程
  • 设计
    解析动作主要针对以下七类符号引用进行。符号引用与常量类型对应关系表如下。
符号引用 常量类型
类或接口 CONSTANT_Class_info
字段 CONSTANT_Fieldref_info
类方法 CONSTANT_Methodref_info
接口方法 CONSTANT_InterfaceMethodref_info
方法类型 CONSTANT_MethodType_info
方法句柄 CONSTANT_MethodHandle_info
调用点限定符 CONSTANT_InvokeDynamic_info
(1) 符号引用
以一组符号来描述所引用的目标,符号可以是任意形式的字面量。
(2) 直接引用
直接指向目标的指针、相对偏移量,或是间接定位到目标的句柄。
3.5 初始化
加载、验证、准备、解析前四个阶段都由 JVM 主导;初始化阶段开始执行字节码程序
  • 设计
    根据 Java 程序代码初始化类变量或其他资源
类初始化的场景
(1) 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,需要先触发其初始化;
(2) java.lang.reflect 对类进行反射调用时,若类没有进行过初始化,则需要触发初始化;
(3) 初始化一个类时,若父类没有初始化,则父类需要触发初始化
(4) JVM 启动时需要指定一个主类,需要首先初始化此类;
(5) java.lang.invoke.MethodHandle 实例解析后的结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,方法句柄对应的类没有被初始化,则先初始化对应的类
4. 类加载器 类加载的加载阶段,是根据类的全限定名获取描述此类的二进制字节流。实现加载动作的代码模块就是类加载器
类加载器和类本身共同保证其在 JVM 的唯一性。同一个类文件使用不同的类加载器处理,得到的类不相等。
4.1 JVM 角度区分两种类加载器
  • 启动类加载器
    启动类加载器 Bootstrap ClassLoader 是 JVM 的一部分,通过 C++ 实现;
  • 非启动类加载器
    其他的类加载器继承于 java.lang.ClassLoader,由 Java 实现,独立于 JVM。
4.2 Java 程序中系统提供的三种类加载器
应用程序类加载器又称系统类加载器,防止概念混淆,文中其他部分引用该加载器,统称为系统类加载器。
一般的,程序中没有自定义类加载器时,默认使用系统类加载器。
自定义类加载器是程序开发者自定义的非启动类加载器。
  • 启动类加载器 Bootstrap ClassLoader
  • 扩展类加载器 Extension ClassLoader
  • 应用程序类加载器 Application ClassLoader
类别 程序可直接引用 支持加载的类库
启动类加载器 JAVA_HOME\lib 目录下的类库参数 -Xbootclasspath 指定路径中的类库
扩展类加载器 JAVA_HOME\lib\ext 目录下的类库系统变量 java.ext.dirs 指定路径中的类库
系统类加载器 系统变量 classpath 指定路径中的类库
自定义类加载器
4.3 类加载器的双亲委派模型
[Java]|不能不了解的,类加载机制
文章图片

图4-1、双亲委派模型图
  • 双亲委派模型
    如图 4-1 双亲委派模型图描述的这种类加载器的层次关系称之为类加载器的双亲委派模型。
  • 工作原理
    一个类加载器收到类加载的请求,会把请求委派给父-类加载器去完成,如果父-类加载器没有找到所需的类无法完成加载时,子-类加载器开始尝试类加载。
类加载器之间存在优先级,使得类也有了优先级,能保证被加载的类在各种类加载器环境中都是同一个类。
前面不是提到类加载器和类本身保证其在 JVM 的唯一性,类通过委派来保证,其能够被更顶端的类加载器所加载。
双亲委派模型是官方推荐的类加载器实现方式,不是强制约束,某些场景下不是双亲委派模型。比如线程提供了上下文类加载器,可以在程序中指定使用某种类加载器。
5. 类文件结构(扩展阅读) 类文件结构包含魔数与文件版本常量池访问标志类索引,夫类索引和接口索引集合字段表集合方法表集合属性表集合
类文件格式支持无符号数和表两种数据类型,类文件的本质就是表。
类文件伪结构的两种数据类型:
无符号数是基本数据类型,表是由多个无符号数或其他表组成的复合数据类型。
无符号数,以 u1、u2、u4、u8 分别代表 1 个、2 个、4 个、8 个字节的无符号数。可以用来描述数字、索引引用、数量值或字符串值。
5.1 魔数与文件版本
类文件是以 8 为字节为基础的二进制流,用记事本打开一个类文件,部分字节信息如下所示。
cafe babe 0000 0034 0013 0a00 0400 0f0a 0004 0010 0700 1107 0012 0100 063c 696e 6974 3e01 0003 2829 5601 0004 436f 6465 ...

魔数
  • 定义
    类文件的前 4 个字节就是魔数,固定十六进制值为 0xcafebabe
文件版本
  • 定义
    类文件的第 5-8 个字节标识类文件版本,次版本号十六进制值为 0x0000,主版本号十六进制值为 0x0034,转为十进制版本号就是 52.0。
5.2 常量池
常量池存放字面量和符号引用
字面量
  • 定义
    此处可以理解为 Java 的常量
符号引用
  • 作用
    包括以下三类常量,(1)类和接口的全限定名; (2)字段的名称与描述符; (3)方法的名称与描述符
5.3 访问标志
  • 定义
    常量池结束后,紧接着的两个字节用来描述访问标志
  • 作用
    用来标识类或接口的访问信息,比如标识类,接口,是否被abstract, public,final 修饰等
5.4 类索引、父类索引与接口索引集合
  • 定义
    类索引、父类索引是一个 u2 类型的数据,接口索引是一组 u2 类型的数据的集合。
  • 作用
    类索引描述当前类,父类索引描述继承了那个类,接口索引集合描述实现了那些接口。由这三项数据来确定当前类的继承关系;
Java 单继承,最多只一个父类, java.lang.Object 没有父类。类索引和父类索引用基本数据类型就够了;java.lang.Object 的父类索引为 0。
Java 多实现,支持实现多个接口,所以要用集合
5.5 字段表集合、方法表集合、属性表集合
字段表集合
  • 定义
    描述接口或类中声明的变量。字段包括类级变量和实例级变量,不包括方法局部变量
  • 结构
    依次为访问标志,名称索引,描述符索引,属性表集合
  • 示例
private static int threadInitNumber; private volatile int threadStatus = 0;

修饰符(访问标志 access_flags)
字段修饰符放在访问标志中,例如 public、static、final、volatile、transient
名称 (名称索引 name_index)
一般的简单名称(例如:方法名 notify,equals,字段名 obj),全限定名(例如: java/lang/Object)
描述符 (描述符索引 descriptor_index)
描述字段的数据类型、方法的参数列表和返回值
属性表集合
存储额外的信息,描述零项或多项额外信息。
方法表集合
  • 定义
    类文件存储格式中,对方法的描述与对字段的描述几乎一致。方法表结构也和字段表结构相同,依次包括了访问标志,名称索引,描述符索引,属性表集合。
  • 示例
private static synchronized int nextThreadNum() { return threadInitNumber++; } private native void start0();

属性表集合
  • 定义
    在类文件,字段表,方法表都可以携带自己的属性集合,用来描述某些场景专有的信息
方法表与字段表区别在于访问标志。
volatile、transient 不能修饰方法,所以方法表的访问标志不包含 ACC_VOLATILE 和 ACC_TRANSIENT 标志。
synchronized、native、strictfp、abstract 可以修饰方法,方法表标志增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP、ACC_ABSTRACT。
【[Java]|不能不了解的,类加载机制】Powered By niaonao

    推荐阅读