ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【DIY系列:Java虚拟机】第11篇:class 文件结构总览——JVM 的“通用语“

【DIY系列:Java虚拟机】第11篇:class 文件结构总览——JVM 的“通用语“ 上一篇【第10篇】Classpath 整合——让 JVM 找到所有 class 文件下一篇【第12篇】魔数与版本号——class 文件的身份证摘要上一章我们找到了 class 文件但读出来是一堆看不懂的数字[202 254 186 190 0 0 0 52 0 143 ...]。这一篇开始我们把这堆数字翻译成有意义的结构。class 文件是 JVM 的通用语——不管你用 Java、Kotlin 还是 Scala 写的代码只要编译成符合规范的 class 文件JVM 就能执行。它也是二进制格式设计的典范学完之后你会理解为什么需要魔数、为什么常量池要单独抽出来。本文先建立全局认知class 文件的八大组成部分、大端序存储、u1/u2/u4 数据类型、表的组织方式最后用十六进制直击一个真实的 class 文件。一、class 文件到底是什么先纠正一个常见误解。你可能以为class 文件就是指磁盘上的.class文件。不准确。JVM 规范里的 class 文件泛指任何格式符合规范的 class 数据【class 文件的来源JVM 规范不限制】 ┌──────────────────────────────────────────────┐ │ class 数据可以从哪来 │ ├──────────────────────────────────────────────┤ │ · 从文件系统读取 .class 文件 ← 我们实现的 │ │ · 从 JAR/ZIP 压缩包中提取 ← 我们实现的 │ │ · 通过网络下载Applet、远程类加载 │ │ · 从数据库中加载 │ │ · 运行时动态生成动态代理、ASM、CGLIB │ │ · 从加密文件中解密得到代码保护 │ └──────────────────────────────────────────────┘ 只要字节流符合 class 文件格式JVM 就能加载重点JVM 规范严格规定了 class 文件的格式但完全不限制 class 文件的来源。这种格式严格、来源自由的设计正是 Java 生态能玩出这么多花样热部署、字节码增强、动态代理的根本原因。为什么格式要严格规定因为编写一次处处运行。如果每个 JVM 实现都有自己的格式那编译后的代码就没法跨平台了。规范把格式钉死才能保证Windows 上编译的 class 文件Linux 上的 JVM 也能跑Oracle JDK、OpenJDK、Azul Zing、我们的 jvmgo都能读同一个 class 文件二、基本数据类型u1、u2、u4class 文件的基本单位是字节整个文件就是一个字节流。为了描述格式JVM 规范定义了三种数据类型类型含义Go 对应类型字节数u11 字节无符号整数uint81u22 字节无符号整数uint162u44 字节无符号整数uint324JVM 规范没有定义 u8但读long/double时需要读 8 字节我们自己加了个readUint64。大端序Big-Endian多字节数据u2、u4在 class 文件中统一用大端序存储。什么是大端序简单说高位字节在前低地址低位字节在后。【大端序 vs 小端序】 假设有一个 u4 值0xCAFEBABE 大端序 Big-Endianclass 文件用这个也叫网络字节序 ┌────────┬────────┬────────┬────────┐ │ CA │ FE │ BA │ BE │ 高字节在前 └────────┴────────┴────────┴────────┘ 偏移 0 偏移 1 偏移 2 偏移 3 最高位 最低位 小端序 Little-Endianx86 CPU 内存存储用这个 ┌────────┬────────┬────────┬────────┐ │ BE │ BA │ FE │ CA │ 低字节在前 └────────┴────────┴────────┴────────┘ 偏移 0 偏移 1 偏移 2 偏移 3 最低位 最高位举例一个 u2 值0x0034十进制 52大端序存储 [0x00, 0x34] 读取方式 0x00 8 | 0x34 0x0034 52 ✅ 如果用小端序错误解析 0x34 8 | 0x00 0x3400 13312 ❌重点这是解析二进制时最容易踩的坑——字节序搞反结果差之千里。好消息是 Go 标准库的encoding/binary包提供了BigEndian.Uint16()/BigEndian.Uint32()我们不用手动移位。表Table的组织方式class 文件里大量使用表来存储相同类型的多条数据。表的结构是【表的结构】 ┌─────────────┬─────────┬─────────┬─────┬─────────┐ │ 表头 (n) │ 表项 1 │ 表项 2 │ ... │ 表项 n │ │ u2 或 u4 │ │ │ │ │ └─────────────┴─────────┴─────────┴─────┴─────────┘ ↑ 表示后面跟着 n 个表项 表头通常是 u2少数情况 u4比如接口索引表┌────────────┬────────┬────────┬────────┐ │ count 3 │ 0x05 │ 0x0A │ 0x11 │ │ (u2) │ (u2) │ (u2) │ (u2) │ └────────────┴────────┴────────┴────────┘ 接口数量 3 常量池索引指向接口名读取逻辑先读 u2 得到 n再循环 n 次读取表项。三、ClassFile 结构全貌JVM 规范用类似 C 语言结构体的语法描述 class 文件格式。整个 class 文件就是一个ClassFile结构ClassFile{u4 magic;// 魔数u2 minor_version;// 次版本号u2 major_version;// 主版本号u2 constant_pool_count;// 常量池计数cp_info constant_pool[constant_pool_count-1];// 常量池u2 access_flags;// 访问标志u2 this_class;// 类索引u2 super_class;// 超类索引u2 interfaces_count;// 接口计数u2 interfaces[interfaces_count];// 接口索引表u2 fields_count;// 字段计数field_info fields[fields_count];// 字段表u2 methods_count;// 方法计数method_info methods[methods_count];// 方法表u2 attributes_count;// 属性计数attribute_info attributes[attributes_count];// 属性表}对应到 Go 的结构体我们的ClassFiletypeClassFilestruct{// magic uint32 // 魔数校验完就不用存了minorVersionuint16// 次版本号majorVersionuint16// 主版本号constantPool ConstantPool// 常量池accessFlagsuint16// 访问标志thisClassuint16// 类索引superClassuint16// 超类索引interfaces[]uint16// 接口索引表fields[]*MemberInfo// 字段表methods[]*MemberInfo// 方法表attributes[]AttributeInfo// 属性表}细节注意magic字段被注释掉了。因为魔数在解析时校验完就完事了没必要占用结构体空间——这是个不错的设计习惯只保留有用的信息。图解class 文件的八大部分【class 文件结构全景图】 偏移 大小 内容 说明 ──────────────────────────────────────────────────────────── 0x00 4 字节 magic 0xCAFEBABE ① 魔数身份标识 0x04 2 字节 minor_version ② 次版本号 0x06 2 字节 major_version ② 主版本号52 JDK 8 ──────────────────────────────────────────────────────────── 0x08 2 字节 constant_pool_count ③ 常量池计数 0x0A 不定 constant_pool[] ③ 常量池重头戏 · 字面量字符串、数字常量 · 符号引用类名、字段名、方法名 ──────────────────────────────────────────────────────────── ... 2 字节 access_flags ④ 访问标志 (public/final/abstract/interface...) ... 2 字节 this_class ⑤ 类索引 → 常量池 ... 2 字节 super_class ⑤ 超类索引 → 常量池 ... 2 字节 interfaces_count ⑥ 接口计数 ... 不定 interfaces[] ⑥ 接口索引表 ──────────────────────────────────────────────────────────── ... 2 字节 fields_count ⑦ 字段计数 ... 不定 fields[] ⑦ 字段表 ... 2 字节 methods_count ⑦ 方法计数 ... 不定 methods[] ⑦ 方法表 ──────────────────────────────────────────────────────────── ... 2 字节 attributes_count ⑧ 属性计数 ... 不定 attributes[] ⑧ 属性表 ────────────────────────────────────────────────────────────八大块可以归成四组分组包含作用① ② 身份信息魔数、版本号确认这是合法 class 文件以及它的版本③ 常量池常量池存放所有字面量和符号引用整个文件最核心的部分④ ⑤ ⑥ 类的元信息访问标志、类索引、超类索引、接口表描述类的访问属性和继承关系⑦ ⑧ 类的成员字段表、方法表、属性表描述类的字段、方法和附加信息四、为什么常量池是核心注意看上面那张图——常量池通常占据 class 文件 60% 以上的空间而且后面几乎所有部分类索引、字段表、方法表、属性表都要引用它。【常量池的中心地位】 ┌──────────────────┐ │ 常量池 │ │ (Constant Pool) │ │ │ │ #1 Utf8 Hello │ │ #2 Class │ │ #3 NameAndType │ │ #4 Methodref │ │ #5 String │ │ ... │ └────────▲─────────┘ │ 所有地方都来引用 ┌───────────────────┼───────────────────┐ │ │ │ ┌────┴────┐ ┌─────┴────┐ ┌──────┴─────┐ │ 类索引 │ │ 字段表 │ │ 方法表 │ │this_class│ │(字段名?) │ │ (方法名?) │ └─────────┘ └──────────┘ └────────────┘ 指向 #2 指向 Utf8 常量 指向 Utf8 常量常量池存两类东西类别内容例子字面量Literal各种常量值字符串Hello、整数123456789、浮点数3.14f符号引用Symbolic Reference类、字段、方法的名字java/lang/Object、main、([Ljava/lang/String;)V重点符号引用是未解析的引用——它只记录一个名字字符串不记录真实的内存地址。JVM 在类加载的解析阶段才会把符号引用转换成直接引用真实指针。这个延迟绑定的设计是 Java 动态性的基础。五、用十六进制直击真实的 class 文件光看结构太抽象我们来解剖一个真实的 class 文件。用xxdLinux/Mac或十六进制编辑器Windows打开 HelloWorld.classxxd HelloWorld.class|head-2000000000: cafe babe 0000 0034 0022 0a00 0600 1409 .......4....... 00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................ 00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....init...() 00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN 00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc 00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable. ...逐字节解读【前 16 字节的解读】 字节序列CA FE BA BE | 00 00 | 00 34 | 00 22 | 0A 00 06 00 14 ... └────┬────┘ └─┬──┘ └─┬──┘ └─┬──┘ │ │ │ │ magic minor major constant 0xCAFEBABE version version pool_count 0x0000 0x0034 0x0022 52 34 (JDK 8) (常量池 有33项) 详细拆解 ┌────────────┬──────────────────────────────────────────┐ │ CA FE BA BE│ 魔数固定值 0xCAFEBABE │ ├────────────┼──────────────────────────────────────────┤ │ 00 00 │ minor_version 0 │ ├────────────┼──────────────────────────────────────────┤ │ 00 34 │ major_version 0x0034 52 → JDK 8 ✅ │ ├────────────┼──────────────────────────────────────────┤ │ 00 22 │ constant_pool_count 0x0022 34 │ │ │ 注意实际常量有 34-1 33 项 │ ├────────────┼──────────────────────────────────────────┤ │ 0A │ 第 1 个常量的 tag 0x0A 10 │ │ │ → CONSTANT_Methodref_info │ ├────────────┼──────────────────────────────────────────┤ │ 00 06 │ class_index 6 │ ├────────────┼──────────────────────────────────────────┤ │ 00 14 │ name_and_type_index 20 │ └────────────┴──────────────────────────────────────────┘看到cafe babe了吗这就是那个著名的魔数。再看后面几行那些3c 696e 6974 3e是什么转成 ASCII3c 69 6e 69 74 3e → i n i t → init 56 → 0x56 86 → 这是 Utf8 常量的长度或 tag 43 6f 64 65 → C o d e → Code属性名class 文件里的字符串就是这么存的——先一个 u2 表示长度后面紧跟 UTF-8 编码的字节。这个我们后面解析常量池时会详细讲。六、我们的解析路线图整个第三模块文章 011-024会按这个顺序实现【class 文件解析路线图】 ① 准备工作 └─ ClassReader字节读取器readUint8/16/32/64、readBytes ↓ ② 文件头文章 012 ├─ magic魔数校验 ├─ minor_version / major_version版本号校验 ↓ ③ 常量池文章 015-020★核心 ├─ 14 种常量类型 ├─ 数值型Integer / Float / Long / Double ├─ 字符串型Utf8 / String └─ 符号引用型Class / NameAndType / Fieldref / Methodref / InterfaceMethodref / MethodType / MethodHandle / InvokeDynamic ↓ ④ 类信息文章 013-014 ├─ access_flags类访问标志 ├─ this_class / super_class类和超类索引 └─ interfaces[]接口索引表 ↓ ⑤ 成员信息文章 014 ├─ fields[]字段表 └─ methods[]方法表 ↓ ⑥ 属性表文章 021-024 ├─ Code方法的字节码 ★最重要 ├─ ConstantValuestatic final 常量值 ├─ Exceptions方法抛出的异常 ├─ LineNumberTable行号映射调试用 ├─ LocalVariableTable局部变量表调试用 ├─ SourceFile源文件名 └─ Deprecated / Synthetic编译器标记对应的 Go 文件文件内容class_reader.goClassReader 字节读取器class_file.goClassFile 结构体 Parse()constant_pool.go常量池constant_info.go14 种常量的具体实现member_info.go字段表和方法表MemberInfoattribute_info.go属性表本篇小结class 文件是 JVM 的通用语核心要点来源自由、格式严格JVM 规范不限定 class 数据从哪来文件/网络/动态生成但严格规定格式三种数据类型u11字节、u22字节、u44字节统一大端序存储表结构表头u2 计数 N 个表项class 文件里到处都是这种结构八大组成部分魔数、版本号、常量池、访问标志、类/超类索引、接口表、字段表、方法表、属性表常量池是核心占文件 60% 空间存字面量和符号引用被其他所有部分引用符号引用延迟绑定只存名字字符串类解析阶段才转成真实指针——这是 Java 动态性的基础下一篇我们开始动手实现ClassReader字节读取器并解析 class 文件的身份证——魔数和版本号。你会看到0xCAFEBABE这个著名数字背后的故事以及 Go 的reslice语法如何让二进制解析变得优雅。上一篇【第10篇】Classpath 整合——让 JVM 找到所有 class 文件下一篇【第12篇】魔数与版本号——class 文件的身份证
返回列表