Version: Next

Class 文件结构

平台无关性基石

为什么要捣鼓出一个 Class 文件

  • 早期编程语言把代码直接编译为机器码 01,直接对应到硬件指令集,就产生了跨平台兼容性问题
  • 捣鼓出 JVM 和配套的 Class 文件,相当于加了一层,做了隔离,实现跨平台性
  • 今天的 JVM 还可以跑 Kotlin、JRuby、JPython 等,主需要用响应的编译器编译到 Class 文件丢给 JVM 就行了
Class 并不是必须是文件

任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)。只是通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它完全不 需要以磁盘文件的形式存在

Class 文件是一组8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割 成若干个8字节进行存储。

根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数,只有两种数据类型:

  • 无符号数:无符号数属于基本的数据类型,以 u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
  • :表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以 _info 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个 前置的 容量计数器 加若干个连续的 数据项 的形式,这时候称这一系列连续的某一类型的数据为某一类型的 集合


魔数与 Class 文件版本

  • Class 文件的 头4个字 节被称为 Magic Number,作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件
  • 紧接着魔数的4个字节 存储的是Class文件的 版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)
  • 使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动
  • 在 JVM 中,Magic Number 的值为 0xCAFEBABE 咖啡宝贝

常量池

Oracle 公司已经为我们准备好一个专门用于 分析Class文件字节码的工具 javap ,在 JDK bin 目录下

  • 紧跟在版本号之后—— 常量池入口
  • 可比喻为 Class 文件的资源仓库
  • 是 Class 文件结构中与其他项目关联最多的数据,通常也是占用 Class 文件空间最大的数据项目之一
  • 是 Class 文件中第一个出现的 类型数据

常量池容量计数

  • 常量池中的常量数目不固定,需要在常量池入口放一个 u2 无符号类型的数据,表示 常量池容量计数值 Constant Pool Count,这个值从 1 开始,除此之外其他集合类型、接口索引集合、字段表集合、方法表集合等都从 0 开始计数
  • 常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21
  • 为什么不从 0 开始: 如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示

常量池两大类型常量

量池中主要存放两大类常量:

  • 字面量(Literal) :比较接近于 Java 语言层面的常量概念,如文本字符串、被声明为final的常量值等
  • 符号引用(Symbolic References):属于编译 原理方面的概念,主要包括 被模块导出或开放的包 Package、类和接口的全限定名 Fully Qualified Name、字段名称和描述符 Descriptor、方法的名称和描述符方法句柄和方法类型 Method Handle | Method Type | Invoke Dynamic、动态调用点 Dynamically-Computed Call Site、动态常量 Dynamically-Computed Constant

Javac 编译时没有连接步骤

  • 像C和C++那样有 连接 这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接
  • Class文件中不会保存各个方法、字段最终 在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的
  • 当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
  • 具体看类加载器

访问标志

紧接着的 2个字节 是访问标志 access_flags

  • 用于识别一些类或 者接口层次的访问信息,包括:
    • 这个Class是类还是接口
    • 是否定义为public类型
    • 是否定义为abstract 类型
    • 如果是类的话,是否被声明为final
    • ...还有注解、枚举等等..

类索引、父类索引、接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合 (int erfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系

  • 类索引 用于确定这个类的全限定名
  • 父类索引 用于确定这个类的父类的全限定名
    • 由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0
  • 接口索引集合:用来描述这个类实现了哪些 接口,这些被实现的接口将按 implements 关键字后 的接口顺序从左到右排列在接口索引集合中,如果 Class 表示的是一个 则对应 extends 关键字

字段表集合

用于描述接口或者类中声明的变量。Java语言中的 字段 Field包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量

  • 字段的作用域(public、private、protected 修饰符)
  • 是实例变量还是类变量(static 修饰符)
  • 可变性 final
  • 并发可见性 volatile
  • 能否被序列化 transient
  • 字段数据类型 (基本类型、对象、数组)
  • 字段名称

  • 上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。

  • 字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述


方法表表集合

方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项

  • 方法里的代码,存储在方法属性表集合中一个名为 Code 的属性里
  • 在Java语言中,要 重载 (Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的 特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值 的不同来对一个已有方法进行重载的

属性表集合

  • 太多了看书吧 - -!