十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JVM夯实之路——从源码到字节码:Class文件结构深入解析

JVM夯实之路——从源码到字节码:Class文件结构深入解析 JVM夯实之路——从源码到字节码Class文件结构深入解析前面几篇主要围绕 JVM 的内存管理展开运行时数据区 ↓ 对象创建与内存布局 ↓ OutOfMemoryError ↓ 垃圾回收算法 ↓ 垃圾收集器接下来视角从“对象如何分配和回收”转向“代码究竟是如何被 JVM 理解和执行的”。要回答这个问题首先就要理解 Java 世界中非常关键的一种文件Class 文件。Java 源代码并不是直接交给 CPU 执行而是首先被编译成 JVM 能够理解的字节码。因此可以把整个过程简单理解为Java Source Code ↓ javac ↓ Class ByteCode ↓ JVM ↓ 程序执行一、为什么 Java 需要字节码计算机最终能够直接执行的是机器指令。不同硬件平台对应的机器指令并不相同例如x86 ARM PowerPC如果程序直接被编译成某一种 CPU 的机器码那么生成出来的程序通常就只能面向对应的平台。也就是说源代码 ↓ x86 Machine Code ↓ 主要面向 x86如果换成另一种体系结构就可能需要重新生成另一套机器代码。Java 希望解决这个问题因此提出了一个非常著名的目标Write Once, Run Anywhere。Java 并不直接把源代码编译成某一种 CPU 的最终机器码而是先生成一种中间表示ByteCode字节码。二、从 Java 源码到 JVM 字节码因此Java 程序的基本执行路径可以表示成.java ↓ javac ↓ .class ↓ JVM其中.java保存程序员编写的 Java 源代码。而.class保存经过编译后的字节码数据。JVM 面对的核心输入并不是 Java 源代码本身而是Class 文件。三、JVM 的“语言无关性”理解 JVM 时有两个比较容易混淆的概念平台无关性 语言无关性这里重点讨论的是语言无关性。所谓语言无关并不是说所有语言可以直接被 JVM 运行。它真正表达的是只要一种语言最终能够编译成符合要求的.class字节码就可以运行在 JVM 上。因此整个模型可以理解为Java │ JRuby │ Groovy │ Scala │ Kotlin │ 其他语言 ↓ 各自的编译器 ↓ Class ByteCode ↓ JVM不同语言 ↓ 不同编译器 ↓ 统一的 .class ↓ JVM四、为什么 JVM 可以不关心源语言原因在于 JVM 只需要识别一种统一格式Class 文件。JVM 不需要知道源代码是不是 Java 使用了哪些 Java 关键字 原始语言采用了什么语法这些语言层面的信息已经由对应编译器转换成了统一的字节码结构。因此Source Language ↓ Compiler ↓ Class File ↓ JVM对 JVM 来说真正重要的是输入的 Class 文件是否满足 JVM 对字节码格式的要求。这也是 Kotlin、Scala、Groovy、JRuby、Clojure 等语言能够运行在 JVM 生态中的重要基础。五、为什么要研究 Class 文件Class 文件是 JVM 执行体系非常重要的基础。后面会涉及到的类加载 验证 解析 执行都建立在 Class 文件之上。所以研究 Class 文件的意义就是理解Java 源代码在经过编译以后究竟以什么样的数据结构交给 JVM。六、Class 文件的本质Class 文件本质上并不是普通文本文件而是一段具有严格格式要求的二进制数据流。它具有几个明显特点没有普通文本意义上的分隔符 数据顺序固定 字节结构严格 使用 Big-Endian也就是大端序。因此Class 文件不能像普通文本一样依赖逗号 空格 换行来判断不同字段之间的边界。JVM 必须严格按照Class 文件规范规定的字段顺序进行解析。七、Class 文件的整体结构Class 文件由一系列严格排列的数据结构组成。原笔记中的总体结构表依次包括magic minor_version major_version constant_pool_count constant_pool access_flags this_class super_class interfaces_count interfaces fields_count fields methods_count methods attributes_count attributes八、u1、u2、u4、u8 是什么阅读 JVM Class 文件结构时经常会看到u1 u2 u4 u8这些表示 JVM 规范中的无符号整数数据类型。对应关系为类型大小u11 Byteu22 Bytesu44 Bytesu88 Bytes这些数据可以用于保存数值 索引 引用定位信息所以看到u4 magic就表示magic字段占用 4 个字节。看到u2 major_version则表示major_version占用 2 个字节。九、Class 文件中的 Table除了固定长度的数据之外Class 文件中还大量存在Table表结构。它通常采用数量 对应数量的数据结构这种设计。例如methods_count methods[methods_count]首先告诉 JVM这里一共有多少个 method随后 JVM 就知道接下来应该读取多少个 method_info这种结构使 JVM 不需要依赖额外的分隔符就可以顺序解析整个二进制文件。十、Magic NumberClass 文件的身份标识Class 文件开头的第一个字段是Magic Number魔数。Class 文件的前 4 个字节固定为0xCAFEBABE它最主要的作用就是让 JVM 快速判断当前文件是不是符合 Class 文件身份要求的数据。可以把它理解成Class File ↓ CAFEBABE ↓ 告诉 JVM “我是一个 Class 文件”原笔记将它类比为其他文件格式中的文件头例如 GIF、JPEG 等格式也存在自己的识别信息。十一、为什么不能只靠 .class 后缀判断文件扩展名其实并不能真正证明文件内部是什么格式。例如一个普通文件完全可以人为改名成Test.class但里面的数据并不一定满足 Class 文件规范。因此JVM 需要从真正的二进制内容中判断文件是否合法。Magic Number 就提供了第一层快速检查读取前 4 Bytes ↓ 是不是 CAFEBABE ↓ 是 继续解析 否 拒绝按照 Class 文件解析十二、Class 文件版本号Magic Number 后面紧跟着Class 文件版本号。它一共占用 4 个字节Minor Version 2 Bytes Major Version 2 Bytes也就是说Class 文件开头可以理解成Magic 4 Bytes ↓ Minor Version 2 Bytes ↓ Major Version 2 Bytes十三、Major Version真正决定 Class 文件主要版本的是Major Version。原笔记中给出的例子是00 00 00 32其中minor 0 major 50对应JDK 6不同版本 Java 编译器生成的 Class 文件会携带对应的 Class Version 信息。十四、JVM 的版本兼容关系原笔记给出的核心规则是高版本 JVM 可以运行较低版本 Class 低版本 JVM 不能直接运行更高版本 Class例如原笔记中JDK 8 Class Version 52 JDK 11 Class Version 55因此JDK 8 JVM ↓ 不能直接运行 JDK 11 生成的 55 版本 Class而更高版本的 JVM 可以处理较低版本的 Class。十五、Class 版本不完全等于当前 JDK 版本这里还有一个容易忽略的问题。使用哪个 JDK 编译并不意味着最终 Class 文件一定使用该 JDK 对应的最高 Class Version。编译时还可以通过目标版本参数控制输出结果。原笔记给出的例子是javac -source 8 -target 8 Test.java即使使用较新的 JDK 执行编译也可以生成对应目标版本的 Class 文件。十六、Constant PoolClass 文件的资源仓库版本号之后就是 Class 文件中非常重要的一部分Constant Pool常量池。原笔记把它称为Class 文件的“资源仓库”。这里保存的不只是我们通常理解的“常量值”还保存大量符号 描述信息 引用关系十七、为什么 Constant Pool 如此重要Java 在编译阶段并不会把所有东西直接转换成最终内存地址。编译阶段更多保存的是Symbolic Reference符号引用。例如某个类叫什么名字 某个方法叫什么名字 方法参数是什么 返回值是什么 某个字段叫什么名字而真正对应到 JVM 运行期中的具体结构需要在后面的类加载、解析和执行过程中进一步确定。因此可以简单理解为编译阶段 ↓ 保存“名字”和“关系” ↓ Constant Pool ↓ 类加载 / 解析 ↓ 转换成 JVM 真正使用的信息十八、Constant Pool 在 Class 文件中的位置常量池紧跟在Magic Number Version之后。而且它是 Class 文件中第一个变长的数据结构。十九、constant_pool_count常量池前面首先会出现constant_pool_count它的类型为u2但这里有一个比较特殊的规则constant_pool_count 实际可使用的常量池容量 1因为 Constant Pool 的索引是从 1 开始。索引0被保留用于表达不引用任何常量池项目。二十、Constant Pool 中保存什么原笔记把 Constant Pool 中的内容大致分成两类1. Literal也就是字面量。例如String int long float double final 常量2. Symbolic Reference也就是符号引用。例如类名 父类名 接口名 字段名 描述符 方法名 描述符 invokedynamic 调用点因此很多与“名字”有关的信息实际上都能够在常量池中找到。二十一、Constant Pool 的不同常量类型Java 需要在 Class 文件中表达很多不同种类的信息基本值 类 字段 方法 接口方法 动态调用 模块 包所以 Constant Pool 并不是只有一种固定结构。原笔记整理了17 种常量类型。每一种 Constant Pool Entry 都有一个共同特征第一个字节一定是tag。可以理解成Constant Pool Entry │ ├── tag │ └── 后续数据其中tag决定后面的字节应该按照什么结构解析。CONSTANT_Utf8_info CONSTANT_Integer_info CONSTANT_Class_info CONSTANT_String_info CONSTANT_Fieldref_info CONSTANT_Methodref_info ...以及对应的tag。二十二、CONSTANT_Class_info 示例原笔记使用CONSTANT_Class_info作为例子。它的结构只有两个主要字段u1 tag u2 name_index其中tag 7表示当前 Constant Pool Entry 是CONSTANT_Class_info而name_index会继续指向常量池中的CONSTANT_Utf8_info例如对应字符串可能是org/fenixsoft/clazz/TestClass原笔记特别指出这里使用的是/而不是 Java 源码中类全限定名常见的.二十三、常量池为什么设计成这种“引用套引用”的结构这也是第一次看 Class 文件时很容易感觉复杂的地方。例如CONSTANT_Class_info ↓ name_index ↓ CONSTANT_Utf8_info ↓ 真正的字符串内容它并不是每一个地方都重复存储完整字符串而是通过Index在不同 Constant Pool Entry 之间建立引用关系。这样就把Java 源代码中的语法信息转换成结构化的二进制数据关系。原笔记将 Constant Pool 概括为 JVM 的一种“语义数据库”。二十四、CONSTANT_Utf8_infoConstant Pool 中非常基础的一种结构是CONSTANT_Utf8_info原笔记中的结构是u1 tag u2 length u1 bytes[length]其中tag 1表示 Utf8 类型。length表示后续字符串数据占用多少字节随后bytes[length]保存真正的字符数据。二十五、为什么 Class 文件中大量内容都依赖 Utf8许多 Java 结构最终都需要名字。例如类名 字段名 方法名 描述符这些字符串信息就需要通过CONSTANT_Utf8_info保存。因此很多 Constant Pool Entry 最终都会通过索引index ↓ CONSTANT_Utf8_info找到对应的字符串描述。二十六、Constant Pool 的整体理解到这里可以把 Constant Pool 简单理解成Constant Pool │ ├── Literal │ ├── Class │ ├── String │ ├── Field Reference │ ├── Method Reference │ ├── Interface Method Reference │ ├── Name And Type │ ├── Dynamic Call Information │ ├── Module │ └── Package不同 Entry 通过Index相互关联。JVM 后续的类加载、解析和执行过程就能够借助这些结构理解 Class 文件原来想表达的类、字段和方法关系。二十七、access_flags类的访问标志Constant Pool 后面紧接着是access_flags它占用2 Bytes也就是u2它的主要作用是描述当前 Class 或 Interface 本身具有哪些修饰特征。例如是不是 public 是不是 final 是不是 interface 是不是 abstract 是不是 annotation 是不是 enum 是不是 module 是不是 synthetic二十八、Class 级别的常见访问标志原笔记中的表格列出了 Class 层面的常见标志例如ACC_PUBLIC ACC_FINAL ACC_SUPER ACC_INTERFACE ACC_ABSTRACT ACC_SYNTHETIC ACC_ANNOTATION ACC_ENUM ACC_MODULE需要注意这里的access_flags描述的是类 / 接口本身。字段和方法虽然也有自己的访问标志结构但它们的具体含义并不完全相同。二十九、access_flags 本质上是 Bit Flagaccess_flags并不是一次只能保存一个状态而是一个Bit Mask位掩码。每一种访问标志占用其中某些 bit。多个标志可以通过Bitwise OR也就是按位或组合到一起。例如原笔记给出的0x0021实际上可以拆成0x0001 0x0020即ACC_PUBLIC ACC_SUPER因此0x0021 ACC_PUBLIC | ACC_SUPER判断某一个标志是否存在时则可以使用对应的Bitwise AND进行检查。三十、Class 文件结构如何串起来到这里可以把这一篇涉及的 Class 文件结构串成Class File │ ├── magic │ ↓ │ CAFEBABE │ ├── minor_version │ ├── major_version │ ↓ │ Class Version │ ├── constant_pool_count │ ├── constant_pool │ ↓ │ 字面量 符号引用 │ ├── access_flags │ ↓ │ 类本身的访问属性 │ ├── this_class ├── super_class ├── interfaces ├── fields ├── methods └── attributes需要注意的是当前这份笔记第 6 章只进一步详细讲到了access_flags。虽然整体结构表已经展示this_class、super_class、interfaces、fields、methods和attributes等结构但原文件没有继续详细展开这些内容因此博客正文不要自行再往下补成另一套教程。三十一、总结这一篇最重要的是理解一条完整转换链路Java Source Code ↓ javac ↓ Class ByteCode ↓ JVMJava 通过 Class 文件这种中间二进制格式使源码和底层具体机器指令之间增加了一层统一的 JVM 表示。而一个 Class 文件本身也不是随意排列的数据而是具有严格结构Magic Number ↓ Version ↓ Constant Pool ↓ access_flags ↓ 其他 Class 数据结构其中Magic Number用于识别 Class 文件Major / Minor Version用于描述 Class 版本Constant Pool保存大量字面量、符号引用和结构关系access_flags使用位标志表示 Class 本身的修饰属性。理解这些结构以后下一步就可以继续回答一个非常自然的问题.class文件已经有了那么 JVM 到底是什么时候、又是怎样把它加载到内存中的这就进入下一部分Class Loading Mechanism类加载机制。
返回列表