文章目录
- [第6章 类文件结构](#第6章 类文件结构)
-
- [6.0 个人感悟](#6.0 个人感悟)
- [6.1 概述](#6.1 概述)
- [6.2 无关性的基石](#6.2 无关性的基石)
- [6.3 Class类文件的结构](#6.3 Class类文件的结构)
-
- [6.3.1 魔数与版本号](#6.3.1 魔数与版本号)
- [6.3.2 常量池](#6.3.2 常量池)
- [6.3.3 访问标志](#6.3.3 访问标志)
- [6.3.4 类索引、父类索引与接口索引集合](#6.3.4 类索引、父类索引与接口索引集合)
- [6.3.5 字段表集合](#6.3.5 字段表集合)
- [6.3.6 方法表集合](#6.3.6 方法表集合)
- [6.3.7 属性表集合](#6.3.7 属性表集合)
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言进步的一大步。 --- 《深入理解Java虚拟机》第3版 周志明
第6章 类文件结构
6.0 个人感悟
-
Class文件的巧妙设计:
软件工程中有句经典的话:任何问题都可以通过增加一个中间层来解决。字节码文件就是硬件和高级语言之间的那个中间层。
- 开发人员:只用关注语言使用,各种语法、语法糖,不用关注到底怎么执行
- 编译部署:平台无关,字节码可以通过JVM到处运行
- JVM:语言无关,无论哪种语言,符合class格式就认
-
向后兼容性的好处:
时至今日,Class文件格式虽然多次更新,但基本只是在原有结构基础上新增内容、扩充功能,并未对已定义的内容做修改。这种设计保证了Java技术一直保持良好的向后兼容性。
-
实践建议:
如果本章内容感觉抽象,建议动手尝试:
- 使用
javap -verbose YourClass.class反编译,对照本章结构理解 - 用十六进制编辑器直接打开
.class文件,逐字节解读 - 尝试编写一个简单的Class文件解析程序,加深对格式的理解
- 使用
6.1 概述
本章围绕一个核心问题展开:
- A: Java源代码(.java文件)经过编译后,究竟变成了什么?
Q: 答案就是Class文件(也叫字节码文件)。虚拟机并不关心这个Class文件是从哪种语言编译来的,它只认这个统一的二进制格式。
6.2 无关性的基石
平台无关性 :
"一次编写,到处运行"(Write Once, Run Anywhere)是Java诞生之初的口号。这个理想最终实现在操作系统应用层上:各种不同平台的Java虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的跨平台运行。这里的字节码就是构成平台无关性的基石。
语言无关性:
JVM只认Class文件,不管它是Java编译来的,还是Kotlin、Scala编译来的。
6.3 Class类文件的结构
整体特点 :
Class文件是以8字节为基础单位的二进制流,各个数据项目严格按顺序紧凑排列,中间没有任何分隔符。当遇到需要占用8个字节以上空间的数据项时,按照高位在前(大端模式)的方式分割成若干个8字节进行存储。Class文件格式采用类似于C语言结构体的伪结构来存储数据。
两种数据类型:无符号数和表
- 无符号数:基本数据类型,用u1、u2、u4、u8分别代表1、2、4、8个字节的无符号数,可描述数字、索引引用、数量值或UTF-8字符串
- 表 :由多个无符号数或其他表构成的复合数据类型,所有表命名都以"_info"结尾(如cp_info、field_info、method_info等)。整个Class文件本质上也可看作一张表。
当需要描述同一类型但数量不定的多个数据时,Class文件格式会使用"前置容量计数器 + 若干个连续数据项"的形式,称为"集合"
ClassFile结构体总览 :
根据《Java虚拟机规范》,Class文件的格式严格限定如下表所示
| 类型 | 名称 | 数量 | 说明 |
|---|---|---|---|
| u4 | magic | 1 | 魔数 |
| u2 | minor_version | 1 | 次版本号 |
| u2 | major_version | 1 | 主版本号 |
| u2 | constant_pool_count | 1 | 常量池计数器 |
| cp_info | constant_pool[constant_pool_count-1] | N | 常量池 |
| u2 | access_flags | 1 | 访问标志 |
| u2 | this_class | 1 | 类索引 |
| u2 | super_class | 1 | 父类索引 |
| u2 | interfaces_count | 1 | 接口计数器 |
| u2 | interfaces[interfaces_count] | N | 接口索引集合 |
| u2 | fields_count | 1 | 字段计数器 |
| field_info | fields[fields_count] | N | 字段表集合 |
| u2 | methods_count | 1 | 方法计数器 |
| method_info | methods[methods_count] | N | 方法表集合 |
| u2 | attributes_count | 1 | 属性计数器 |
| attribute_info | attributes[attributes_count] | N | 属性表集合 |
6.3.1 魔数与版本号
魔数 :
头4个字节,固定值 0xCAFEBABE(咖啡宝贝),用于确定文件是否为合法的Class文件。使用魔数而非扩展名是出于安全考虑。
版本号 :
紧接着的4个字节,第5、6为次版本号,第7、8为主版本号
- 高版本JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件
- JDK 8 → 主版本号 52(0x0034),JDK 11 → 55(0x0037)
- JDK 12之后,次版本号为65535时表示"技术预览版"功能
6.3.2 常量池
常量池是Class文件中与其他项目关联最多的数据,也是第一个出现的表类型数据项目,可理解为"资源仓库"。
- 常量池计数器:u2类型,从1开始计数,第0项留空(表示"不引用任何常量池项目")。
- 两大类常量 :
- 字面量:如文本字符串、final常量值等。
- 符号引用:包括包、类和接口的全限定名、字段名称和描述符、方法名称和描述符、方法句柄、动态调用点等。
- 常量池中每一项都以一个u1类型的tag开头,JDK 1.7之后共有14种不同的表结构。
6.3.3 访问标志
常量池之后的两个字节(access_flags),用于识别类或接口层次的访问信息:是类还是接口?是否为public?是否为abstract?如果是类,是否声明为final?共有16个标志位,当前定义了8个,未使用的必须为0
6.3.4 类索引、父类索引与接口索引集合
- 类索引(this_class):确定类的全限定名。
- 父类索引(super_class):确定父类的全限定名。
- 接口索引集合(interfaces[]):描述该类实现的接口,按implements语句从左到右排列。
6.3.5 字段表集合
字段表(field_info)描述接口或类中声明的变量(类级变量和实例级变量),不包括方法内的局部变量。
- 字段表集合不会列出从超类或父类接口继承的字段。
- 字段表结构包含:访问标志、名称索引、描述符索引、属性表集合。名称索引和描述符索引指向常量池中的字符串常量。
6.3.6 方法表集合
方法表(method_info)结构与字段表类似,但访问标志中没有volatile和transient(对方法无意义)
- 方法表结构包含:访问标志、名称索引、描述符索引、属性表集合。
- 字节码指令存储位置:方法表的属性表集合中的
Code属性------这里存放着真正的字节码指令序列。这也印证了:字节码指令是字节码文件的核心内容,存在于每个方法的Code属性中。
6.3.7 属性表集合
属性表(attribute_info)是Class文件中最灵活的部分,在Class文件、字段表、方法表中都可携带,用于描述某些场景专有的信息。与Class文件其他部分严格顺序不同,属性表集合的顺序要求较宽松
常见属性:
- Code属性:存储方法体的字节码指令(最关键)。
- Exceptions属性:方法抛出的异常。
- LineNumberTable属性:源码行号与字节码指令的对应关系(用于调试)。
- LocalVariableTable属性:方法局部变量的描述。
- SourceFile属性:源文件名。
- ConstantValue属性:final常量值的初始化。
- InnerClasses属性:内部类信息。
- Deprecated及Synthetic属性。