类文件结构
Class 文件的核心作用
Class 文件是 Java 虚拟机可理解的字节码文件,是 Java 实现跨平台的关键。它不依赖特定处理器,仅面向虚拟机,多种语言(如 Kotlin、Groovy、Scala 等)可通过各自编译器编译为 Class 文件,最终在 JVM 上运行,成为不同语言与 JVM 之间的桥梁。
Class 文件整体结构
Class 文件结构固定,遵循 ClassFile 结构体定义,按顺序包含 10 个核心组件:魔数、类文件版本号、常量池、访问标志、当前类 / 父类 / 接口索引集合、字段表集合、方法表集合、属性表集合,各组件通过固定长度的无符号整数(u1、u2、u4 分别表示 1、2、4 字节)标识长度和内容,结构严谨且无冗余。
核心组件详解
(1)魔数(Magic Number)
位于 Class 文件开头 4 字节,固定值为 0xCAFEBABE,作用是标识文件是否为 JVM 可识别的 Class 文件,非该魔数开头的文件会被 JVM 拒绝加载。
(2)类文件版本号
紧跟魔数的 4 字节,前 2 字节为次版本号(Minor Version),后 2 字节为主版本号(Major Version)。主版本号随 Java 大版本递增(如 Java 8 对应主版本号 52),高版本 JVM 可兼容低版本 Class 文件,低版本 JVM 无法运行高版本 Class 文件,需保证开发与生产环境 JDK 版本一致。
(3)常量池(Constant Pool)
版本号之后的核心区域,是 Class 文件中占用空间最大的部分之一。常量池计数器(constant_pool_count)从 1 开始计数,实际常量数量为计数器值减 1(索引 0 表示 "不引用任何常量"),主要存储字面量(文本字符串、final 常量值等)和符号引用(类 / 接口全限定名、字段 / 方法的名称和描述符等)。每个常量以 1 字节的标志位(tag)区分类型,共 14 种常量类型(如 CONSTANT_UTF8_info 表示 UTF-8 字符串、CONSTANT_MethodRef_info 表示方法符号引用)。
(4)访问标志(Access Flags)
占 2 字节,用于标识类 / 接口的访问属性,如是否为 public、final、abstract、接口、枚举、注解等(如 ACC_PUBLIC 对应 0x0001、ACC_INTERFACE 对应 0x0200),通过标志位组合表示多重属性。
(5)当前类、父类、接口索引集合
- 当前类索引(this_class)和父类索引(super_class)各占 2 字节,分别指向常量池中类的全限定名,Java 单继承特性决定父类索引仅一个(除 java.lang.Object 外,所有类的父类索引均非 0)。
- 接口索引集合包含接口计数器(interfaces_count)和接口索引数组,按 implements/extends 后的顺序存储实现的接口索引,标识类实现的所有接口。
(6)字段表集合(Fields)
存储类的字段信息(类变量和实例变量,不含局部变量),包含字段计数器(fields_count)和字段表数组(field_info)。每个字段表包含访问标志(如 public、static、final、volatile 等)、名称索引(指向常量池中的字段名)、描述符索引(指向常量池中的字段数据类型描述)、属性计数器及属性表,描述字段的修饰符、名称和类型。
(7)方法表集合(Methods)
存储类的方法信息,结构与字段表类似,包含方法计数器(methods_count)和方法表数组(method_info)。每个方法表包含访问标志(如 public、static、synchronized、native、abstract 等,无 volatile 和 transient 标志)、名称索引(方法名)、描述符索引(方法参数和返回值类型描述)、属性计数器及属性表,描述方法的修饰符、名称、参数和返回值信息。
(8)属性表集合(Attributes)
可附加在 Class 文件、字段表、方法表中,用于存储额外专有信息(如方法体代码、异常表、注解等)。属性表无固定顺序,JVM 会忽略不认识的属性,编译器可自定义属性,增强扩展性。
Class 文件查看工具
可通过 javap -v 类名 命令查看 Class 文件的详细结构(如版本号、常量池、访问标志等),或使用 IDEA 插件 jclasslib 直观查看类的基本信息、常量池、字段、方法、属性等内容,便于字节码分析。
类加载过程
类的生命周期
类的生命周期包含 7 个阶段,按顺序为加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证、准备、解析统称为连接(Linking)阶段,核心流程为 "加载→连接→初始化→使用→卸载"。
加载阶段
加载是类加载的第一步,核心完成三件事:通过全类名获取类的二进制字节流(来源灵活,如 JAR 包、网络、动态生成等);将字节流的静态存储结构转换为方法区的运行时数据结构;在内存中生成代表该类的 Class 对象,作为方法区数据的访问入口。加载由类加载器完成,遵循双亲委派模型(可打破),非数组类的加载可控性强,可自定义类加载器重写 loadClass () 方法控制字节流获取,加载与连接阶段部分动作可能交叉进行。
验证阶段
验证是连接阶段的第一步,目的是确保 Class 文件字节流符合《Java 虚拟机规范》,避免恶意代码危害虚拟机安全,耗费资源较多但可通过 -Xverify:none(JDK13 后 deprecated)关闭大部分验证。验证分四个阶段:文件格式验证(检查字节流格式,如魔数、版本号)、元数据验证(语义检查,如类是否有父类、是否继承 final 类)、字节码验证(程序逻辑检查,如参数类型、类型转换合法性)、符号引用验证(检查依赖类 / 方法 / 字段是否存在及权限,发生在解析阶段,失败抛出 IllegalAccessError 等异常)。
准备阶段
准备阶段为类变量(静态变量)分配内存并设置初始零值,内存分配位置随版本变化:JDK7 前在永久代(方法区实现),JDK7 及后随 Class 对象存于堆中,实例变量不在此阶段分配(对象实例化时分配于堆)。初始零值为数据类型默认值(如 int 为 0、boolean 为 false),特殊情况:被 final 修饰的静态常量,准备阶段直接赋值为指定值(非零值)。
解析阶段
解析阶段将常量池中的符号引用替换为直接引用:符号引用是无歧义描述目标的符号(与内存布局无关,目标未必已加载),直接引用是指向目标的指针、偏移量或句柄(与内存布局相关,目标必已存在)。解析动作针对类 / 接口、字段、类方法等 7 类符号引用,例如将方法的符号引用转换为方法表中的偏移量,确保方法可被调用。
初始化阶段
初始化是类加载的最后一步,核心是执行编译器自动生成的 <clinit>() 方法,真正执行类中定义的 Java 代码。虚拟机保证 <clinit>() 方法线程安全(带锁),多线程初始化可能导致阻塞。必须触发初始化的 6 种场景:执行 new、getstatic 等 4 条字节码指令;反射调用类;初始化子类时父类未初始化;虚拟机启动时的主类(含 main 方法);使用 MethodHandle/VarHandle 前初始化目标类;接口有默认方法时,其实现类初始化前先初始化接口。
类卸载阶段
类卸载即 Class 对象被 GC,需满足三个条件:堆中无该类的任何实例;该类无任何地方被引用;加载该类的类加载器实例已被 GC。JVM 自带的类加载器(如 BootstrapClassLoader)加载的类不会卸载,自定义类加载器加载的类满足条件时可被卸载。