Java JVM Class 文件


Java 的口号 “一次编写, 到处运行 (Write Once, Run Anywhere)” 的基础: JVM 和 所有平台都统一支持的程序存储格式 – 字节码 (Byte Code)。
只要在对应的平台安装对应的 JVM, 将我们编写的源码编译为 Class 文件, 就能达到了一次编写, 导出运行的目标, 中间的所有细节由不同平台的 JVM 进行处理即可。

这个过程中最重要的产物就是 Class 文件, 本文将简单地介绍一下 Class 文件的结构和内容。

1 Java 文件到 Class 文件

我们编写的 Java 源代码需要借助 Javac 编译器, 才能编译成 JVM 能识别的字节码文件, 流程如图所示

Alt 'Java 源代码到 Class 文件'

Javac 是一种编译器, 能将一种语言规范转化成另外一种语言规范, 通常编译器都是将便于人理解的语言规范转化成机器容易理解的语言规范,
如 C/C++ 或者汇编语言都是将源代码直接编译成目标机器码, 这个目标机器代码是 CPU 直接执行的指令集合, 这些指令集合也就是底层的一种语言规范。

Javac 的编译器也是将 Java 这种对人非常友好的编程语言编译成对所有机器都非常友好的一种语言, 这种语言不是针对某种机器或某个平台。
怎么消除不同种类, 不同平台之间的差异这个任务就由 JVM 来完成, 而 Javac 的任务就是将 Java 源代码语言转化为 JVM 能够识别的一种语言,
然后由 JVM 再转化成当前这个机器能够识别的机器语言。

回归到 Java, Javac 的任务就是将 Java 源代码编译成 Java 字节码, 也就是JVM能够识别的二进制代码, 从表面看是将 .java 文件转化为 .class 文件。
而实际上是将 Java 源代码转化成一连串二进制数字, 这些二进制数字是有格式的, 只有 JVM 能够真确的识别他们到底代表什么意思。

编译器把一种语言规范转化为另一种语言规范的这个过程需要哪些步骤? 回答这个问题需要参照《编译原理》, 总结过程如下:

  1. 词法分析:读取源代码, 一个字节一个字节的读进来, 找出这些词法中我们定义的语言关键词如:if、else、while 等, 识别哪些 if
    是合法的哪些是不合法的。这个步骤就是词法分析过程。

词法分析的结果:就是从源代码中找出了一些规范化的 token 流, 就像人类语言中, 给你一句话你要分辨出哪些是一个词语, 哪些是标点符号, 哪些是动词, 哪些是名词。

  1. 语法分析:就是对词法分析中得到的 token 流进行语法分析, 这一步就是检查这些关键词组合在一起是不是符合 Java 语言规范。如 if
    的后面是不是紧跟着一个布尔型判断表达式。

语法分析的结果:就是形成一个符合 Java 语言规定的抽象语法树, 抽象语法树是一个结构化的语法表达形式, 它的作用是把语言的主要词法用一个结构化的形式组织在一起。
这棵语法树可以被后面按照新的规则再重新组织。

  1. 语义分析:语法分析完成之后也就不存在语法问题了, 语义分析的主要工作就是把一些难懂的, 复杂的语法转化成更简单的语法。就如难懂的文言文转化为大家都懂的百话文,
    或者是注释一下一些不懂的成语。

语义分析结果:就是将复杂的语法转化为简单的语法, 对应到 Java 就是将 foreach 转化为 for
循环, 还有一些注释等。最后生成一棵抽象的语法树, 这棵语法树也就更接近目标语言的语法规则。

  1. 字节码生成:将会根据经过注释的抽象语法树生成字节码, 也就是将一个数据结构转化为另外一个数据结构。就像将所有的中文词语翻译成英文单词后按照英文语法组装成英文语句。
    代码生成器的结果就是生成符合 Java 虚拟机规范的字节码。

这个过程中的需要的组件如下图所示

Alt '编译器编译需要的组件'

从上面的描述中我们知道编译就是将一种语言通过分析分解, 再按照一定的方式先形成一个简单的框架 (将 Java 源文件的字节流转化为对应的 token 流),
然后在通过详细的分析按照一定的规定在这个框架里添加东西使这个 token 流形成更加结构化的语法树 (就是将前面生成的token流中的一个个单词组装成一句话),
但是这棵树离我们的目标 – Java 字节码还有点差距。

所以再进行语义分析使那棵粗糙的树更加完整完善 (给类添加默认的构造函数, 检查变量在使用前有没有初始化, 检查操作变量类型是否匹配),
然后 Javac 编译器调用 com.sun.tools.javac.jvm.Gen 类遍历这棵语法树将 Java 方法中的代码块转换成符合 JVM 语法的命令形式的二进制数据。
按照 JVM 的文件组织格式将字节码输出到以 class 为扩展名的文件中, 也就是生成最终的 Java 字节码。

词法分析: 将关键词组织成 token 流即检查源码中的的关键词是否正确并组织成 token 流。
语法分析: 检查源码是否符合 Java 语法规范并将词组成语句。
语义分析: 简化复杂的语法, 检查变量类型是否合法。
代码生成器: 遍历这棵树生成符合 JVM 规范的代码。

2 Class 文件的结构

2.1 Class 文件的特点

任何一个 Class 文件都对应着唯一的一个类或接口的定义信息 (在 JDK9 以后, Java 开始模块化, 出现了 package-info.java, moudle-info.java 这些属于反例, 完全属于描述性的),
但是类或接口并不一定都得定义在文件里 (比如类或接口可以动态生成, 直接送入类加载器中)。

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

在计算机系统中, 我们是以字节为单位的, 每个地址单元都对应着一个字节, 一个字节为 8 bit。 存储 8 个字节的内容, 没有问题。
但是需要存储超过 8 个字节的时候, 比如 16 字节, 32 字节 时, 怎么办呢?

这时理所当然的在用几个 8 字节进行凑就可以了, 比如 16 个字节用 2 个 8 字节凑, 就满足了。 但是将这 16 个字节分成 2 个 8 字节进行存储时, 出现了 2 种方式的存放方式。
big-endian (数据的低位保存在内存的高地址中, 而数据的高位保存在内存的低地址中) 和 little-endian (数据的低位保存在内存的低地址中, 而数据的高位保存在内存的高地址中)。

比如我们现在有一个 16 Bit 的数据 0x1234, 需要 2 个 8 字节的位置进行, 完全按照内容顺序存储,
12 存储在内存的前面, 也就是高位, 34 存储在后面, 也就是低位, 内存中存储的顺序为 1234, 这个是 little-endion。
但是反着过来, 将 34 存储在内存的前面, 即高位, 12 反而存在内存的后面, 即低位, 存储存储顺序变为 3412, 这个就是 big-endian。

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

无符号数属于基本的数据类型, 以 u1/u2/u4/u8 来分别代表 1 个字节 / 2 个字节 / 4 个字节 / 8 个字节的无符号数, 无符号数可以用来描述数字,
索引引用, 数量值或者按照 UTF-8 编码构成字符串值。

表由多个无符号数或者其他表作为数据项构成的复合数据类型, 为了便于区分, 所有表的命名都习惯性地以 “_info“ 结尾。
表用于描述有层次关系的复合结构的数据, 整个 Class 文件本质上也可以视作是一张表, 这种表的内容大致如下:

名称 类型 数量
magic u4 1
minor_version u2 1
major_version u2 1
constant_pool_count u2 1
constant_pool cp_info constant_pool_count - 1
access_flags u2 1
this_class u2 1
super_class u2 1
interfaces_count u2 1
interfaces u2 interfaces_count
fields_count u2 1
fields filed_info fields_count
methods_count u2 1
methods method_info methods_count
attributes_count u2 1
attributes attribute_info attributes_count

下面逐个看一下它们都是什么含义吧。

2.1.1 magic/minor_version/major_version

每个 Class 文件的头 4 个字节被称为魔数 (magic_version), 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
Class 文件的魔数为 “0xCAFEBABE”。

接着下面的 4 个字节 (minor_version + major_version), 第 5, 6 位表示次版本号, 7, 8 表示主版本号。

用 JDK8 和 JDK14 编译同一个 Java 文件, 他们的版本号字节分别为 0000 00340000 003a。 这个版本号会影响到 JVM 能否执行这个文件。
每个 JDK 都有自己支持支持的版本号范围, 在这个范围的前提下, 能够向下兼容低版本的, 但是一定不能执行超过自己支持的高版本号的文件。

2.1.2 常量池

和常量池相关的 constant_pool_countconstant_pool
constant_pool_count 这个容量计数是从 1 开始, 而不是 0, 如果常量池数量的值为 22, 这就是代表常量池中有 21 项。

常量池中主要存放两大类常量: 字面量 (Literal) 和符号引用 (Symbolic References)。

字面量比较接近于 Java 语言层面的常量概念, 如文本字符串, 被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,
主要包括下面几类常量

  1. 被模块导出或者开放的包 (Package)
  2. 类和接口的全限定名 (Fully Qualified Name)
  3. 字段的名称和描述符 (Descriptor)
  4. 方法的名称和描述符
  5. 方法句柄和方法类型 (Method Handle, Method Type, Invoke Dynamic)
  6. 动态调用点和动态常量 (Dynamically-Computed Call Site, Dynamically-Computed Constant)

Java 代码在进行 Javac 编译的时候, 在 Class 文件中不会保存各个方法, 字段最终在内存中的布局信息。
这些字段, 方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址, 也就无法直接被虚拟机使用的。
当虚拟机做类加载时, 将会从常量池获得对应的符号引用, 再在类创建时或运行时解析, 翻译到具体的内存地址之中。

常量池中每一项常量都是一个表, 截至 JDK13, 常量表中分别有 17 种不同类型的常量。
这 17 类表起始的第一位是 u1 类型的标志位 (一般叫做 tag), 代表着当前常量属于什么类型的常量类型。

类型 描述 标志
CONSTANT_Utf8_info UTF-8 编码的字符串 1
CONSTANT_Integer_info 整形字面量 3
CONSTANT_Float_info 浮点型字面量 4
CONSTANT_Long_info 长整型字面量 5
CONSTANT_Double_info 双精度浮点型整形 6
CONSTANT_Class_info 类或接口的符号引用 7
CONSTANT_String_info 字符串类型字面量 8
CONSTANT_Fieldref_info 字段的符号引用 9
CONSTANT_Methodref_info 类中方法的符号引用 10
CONSTANT_InterfaceMethodref_info 接口中方法的符号引用 11
CONSTANT_NameAndType_info 字段或方法的部分符号引用 12
CONSTANT_MethodHandle_info 方法句柄 15
CONSTANT_MethodType_info 方法类型 16
CONSTANT_Dynamic_info 一个动态计算常量 17
CONSTANT_InvokeDynamic_info 一个动态方法调用点 18
CONSTANT_Module_info 一个模块 19
CONSTANT_Package_info 一个模块中开放或导出的包 20

字符串表 CONSTANT_Utf8_info 的内容如下

名称 类型 数量
tag u1 1
length u2 1
bytes u1 length

在 Class 文件, 经过了魔数, 版本, 常量池的数量, 紧接着的就是具体的常量项。
接着往后找,

假设找到第一个字节, 值为 1, 这表示第一个常量为字符串,
接着往下找第 2, 3 个字节 (字符串常量的长度用的是 16 个字节表示), 得到这个字符串常量的长度占了多少个字节,
再往后找对应的字节数, 这个就是字符串的内容了

这样就能得到第一个常量的内容。

一个小知识: u2 类型能表达的最大值 65535, 也就是 64 KB, 如果我们定义了一个字符串常量/方法名等超过了这个临界值, 会编译失败

类或接口的符号引用表 CONSTANT_Class_info 的内容如下

名称 类型 数量
tag u1 1
name_index u2 1

tag 同上, 而 name_index 是常量池的索引值, 它指向常量池中一个 CONSTANT_Utf8_info 类型常量, 此常量代表了这个类 (或者接口) 的全限定名。
其他类型的常量表差不多, 就不列举了, 在实际中, 我们可以通过各种工具, 对 Class 文件进行分析, 不需要通过如此计算每个字节,
各种转换, 最接近的工具就是 JDK 自带的 javac

2.1.3 访问符标志

经过常量池后, 紧接着的是 u2 表示的访问符标识 (access_flags), 这个标志用于识别一些类或者接口层次的访问信息,
包括: 这个 Class 是类还是接口 / 是否定义为 public 类型 / 是否定义为 abstract 类型 / 如果是类的话, 是否被声明为 final 等

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否被声明为 final, 只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义, invokespecial 指令在 JDK1.0.2 发生了改变, 所以为了不混淆, JDK1.0.2 以后这个必须为 true
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 标识这是一个 abstract 的类型, 抽象类或接口这个为 true, 其他为 false
ACC_SYNTHETIC 0x1000 标识这个类是否由用户代码生成
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模板

access_flags 中一共有 16 个标志位可以使用, 当前只定义了其中 9 个, 其他没用到的都为 0 (2 个字节, 16 位, 所以上限为 16 个标识)。

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

一个 Java 类的继承 / 实现关系的确定是由下面 4 个参数决定的 this_class, super_class, interface_count, *
interfaces*

this_class: 类索引, 用于确定这个类的全限定名
super_class: 父类索引, 用于确定这个类的父类的全限定名, 除了 java.lang.Object 外, 所有 Java 类的父类索引都不为空
interface_count: 接口个数, 用于确定这个类的接口个数
interfaces: 接口索引集合, 用来描述这个类实现了哪些接口, 按照 implements 关键字从左到右排列 (当然如果当前是一个接口类, 那么就是 extends 关键字的后面)

名称 类型 数量
this_class u2 1
super_class u2 1
interfaces_count u2 1
interfaces u2 interfaces_count

2.1.5 字段表集合

字段表 (field_info) 用于描述接口或者类中声明的变量。

在 Java 中一个字段定义, 会涉及下面几个方面

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

字段表的结构

名称 类型 数量
acc_flags u2 1
name_index u2 1
descriptor_index u2 1
attributes_count u2 1
attributes attributes_info attributes_count

字段修饰符放在 access_flags 项目中, 它与类中的 access_flags 项目是非常类似的, 都是一个 u2 的数据类型, 其中可以设置的标志位和含义。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为 public 类型
ACC_PRIVATE 0x0002 字段是否为 private 类型
ACC_PROTECTED 0x0004 字段是否为 protected 类型
ACC_STATIC 0x0008 字段是否为 static
ACC_FINAL 0x0010 字段是否为 final
ACC_VOLATILE 0x0040 字段是否为 volatile
ACC_TRANSIENT 0x0080 字段是否 transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生
ACC_ENUM 0x4000 字段是否为 enum

很明显, 由于语法规则的约束, ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED 三个标志最多只能选择其一, ACC_FINAL, ACC_VOLATILE 不能同时选择。
接口之中的字段必须有 ACC_PUBLIC, ACC_STATIC, ACC_FINAL 标志, 这些都是由 Java 本身的语言规则所导致的。

在 access_flags 标志的是两项索引值

name_index
descriptor_index

它们都是对常量池项的引用, 分别代表着字段的简单名称以及字段和方法的描述符。

全限定名: 就是包名 + 类名的字符串, 将里面的 . 替换为 /, 同时在末尾加上 ; 表示结束的
简单名称: 就是没有任何类型和参数修饰的方法或字段
描述符: 用来描述字段的数据类型, 方法的参数列表 (包括数量, 类型以及顺序) 和返回值

描述符标识字符含义

标识字符 含义
B 基础数据类型 byte
C 基础数据类型 char
D 基础数据类型 double
F 基础数据类型 float
I 基础数据类型 int
L 基础数据类型 long
S 基础数据类型 short
Z 基础数据类型 boolean
V 特殊类型 void
L 对象类型 如 Ljava/lang/Object

对于数组类型, 每一维度将使用一个前置的 “[“ 字符来描述,
如一个定义为 “java.lang.String[][]” 类型的二维数组将被记录成 “[[Ljava/lang/String;”, 一个整型数组”int[]” 将被记录成 “[I”。

用描述符来描述方法时, 按照先参数列表, 后返回值的顺序描述, 参数列表按照参数的严格顺序放在一组小括号 “()” 之内,
比如方法 “int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)” 的描述符为 “([CII[CIII])I”,
再如 “String toString(String str)” 的描述符为 “(Ljava/lang/String;)Ljava/lang/String;”

在描述符后面的就是属性表集合了, 用于存储一些额外的信息。
字段表可以在属性表中附加描述零至多项的额外信息, 比如默认值之类的, 具体内容后面的属性表在聊。

字段表集合中不会列出从父类或者父接口中继承而来的字段, 但有可能出现原本 Java 代码之中不存在的字段,
譬如在内部类中为了保持对外部类的访问性, 编译器就会自动添加指向外部类实例的字段。

2.1.6 方法表集合

Class 文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式, 方法表的结构如同字段表一样,
依次包括访问标志 (access_flags), 名称索引 (name_index), 描述符索引 (descriptor_index), 属性表集合 (attributes) 几项

方法表的结构

名称 类型 数量
acc_flags u2 1
name_index u2 1
descriptor_index u2 1
attributes_count u2 1
attributes attributes_info attributes_count

因为 volatile 关键字和 transient 关键字不能修饰方法, 所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志。
与之相对, synchronized, native, strictfp 和 abstract 关键字可以修饰方法, 方法表的访问标志中也相应地增加了
ACC_SYNCHRONIZED, ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 public 方法
ACC_PRIVATE 0x0002 是否为 private 方法
ACC_PROTECTED 0x0004 是否为 protected 方法
ACC_STATIC 0x0008 是否为 static 方法
ACC_FINAL 0x0010 是否为 final 方法
ACC_SYNCHRONIZED 0x0020 是否为 synchronized 方法
ACC_BRIDGE 0x0040 是否为编译器生成的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 是否为 native 方法
ACC_ABSTRACT 0x0400 是否为 abstract 方法
ACC_STRICT 0x0800 是否为 strictfp 方法
ACC_SYNTHETIC 0x1000 方法是否为编译器自动产生

2.1.7 属性表集合

属性表 (attribute_info) 与 Class 文件中其他的数据项目要求严格的顺序, 长度和内容不同, 不再要求各个属性表具有严格顺序。

属性表的结构

名称 类型 数量
attribute_name_index u2 1
attribute_length u1 1
info u1 attribute_length

虚拟机规范预定义的属性

属性名称 使用位置 含义
Code 方法表 Java 代码编译成的字节码指令
ConstantValue 字段表 由 final 关键字定义的常量值
Deprecated 类, 方法表, 字段表 被声明为 deprecated 的类, 方法, 字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部类或匿名类时才能拥有这个属性, 这个属性用于标志这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code 属性 Java 源码的行号和字节码指令的对应关系
LocalVariableTable Code 属性 方法的局部变量描述
StackMapTable Code 属性 供新的类型检查验证器 (Type Checker) 检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类, 方法表, 字段表 用于支持泛型下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类, 方法表, 字段表 标识方法或字段是编译器自动生成的
LocalVariableTypeTable 使用特征签名替代描述符, 是为了引入泛型语法后能描述泛型参数化类型
RuntimeVisibleAnnotations 类, 方法表, 字段表 为动态注解提供支持, 用于指明哪些注解是运行时可见的
RuntimeInVisibleAnnotations 类, 方法表, 字段表 为动态注解提供支持, 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations 方法表 作用和 RuntimeVisibleAnnotations 相似, 只不过这个只能用于方法参数
RuntimeInVisibleParameterAnnotations 方法表 作用和 RuntimeInVisibleAnnotations 相似, 只不过这个只能用于方法参数
AnnotationDefault 方法表 用于记录注解类元素的默认值
BoostrapMethods 类文件 用于保存 invokedynamic 指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations 类, 方法表, 字段表, Code 属性 为实现 JSR 308 中新增的类型注解提供的支持, 用于指明哪些注解是运行时可见的
RuntimeInVisibleTypeAnnotations 类, 方法表, 字段表, Code 属性 为实现 JSR 308 中新增的类型注解提供的支持, 用于指明哪些注解是运行时不可见的
MethodParameters 方法表 用于支持 (编译时加入 -parameters 参数) 将方法名称编译进 Class 文件, 并可运行时获取
Module 用于记录一个 Module 的名称和相关的信息 (requires, exports, opens, uses, provides)
ModulePackages 用于记录一个模块中所有被 exports 或 opens 的包
ModuleMainClass 用于指定一个模块的主类
NestHost 用于支持嵌套类的反射和访问控制 API, 一个内部类通过该属性得知自己的宿主类
NestMembers 用于支持嵌套类的反射和访问控制 API, 一个内部类通过该属性得知自己有哪些内部类
  • Code 属性

Code 属性表的结构

名称 类型 数量
attribute_name_index u2 1
attribute_length u4 1
max_stack u2 1
max_locals u2 1
code_length u4 1
code u1 cide_length
exception_table u2 1
attributes_count u2 1
attributes attributes_info attributes_count

attribute_name_index 是一项指向 CONSTANT_Utf8_info 型常量的索引, 此常量值固定为 “Code”, 它代表了该属性的属性名称,
attribute_length 指示了属性值的长度。

max_stack 代表了操作数栈 (Operand Stack) 深度的最大值。 在方法执行的任意时刻, 操作数栈都不会超过这个深度。
虚拟机运行的时候需要根据这个值来分配栈帧 (Stack Frame) 中的操作栈深度

max_locals 代表了局部变量表所需的存储空间。 在这里, max_locals 的单位是变量槽 (Slot), 变量槽是虚拟机为局部变量分配内存所使用的最小单位。
对于 byte, char, float, int, short, boolean 和 returnAddress 等长度不超过 32 位的数据类型, 每个局部变量占用一个变量槽, 而
double 和 long 这两种 64 位的数据类型则需要两个变量槽来存放。 方法参数 (包括实例方法中隐藏参数 “this”),
显式异常处理程序的参数 (Exception Handler Parameter, 就是 try-catch 语句中 catch 块中所定义的异常),
方法体中定义的局部变量都需要依赖局部变量表来存放。 并不是在方法中用了多少个局部变量, 就把这些局部变量所占变量槽数量之和作为
max_locals 的值, 在代码实际运行中, 槽有时可以重用。

code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。 code_length 代表字节码长度, code
是用于存储字节码指令的一系列字节流。
注: 虽然 code_length 的类型为 u4, 理论上最大值可以达到 2 的 32 次幂, 但是 《Java虚拟机规范》 中明确限制了一个方法不允许超过
65535 条字节码指令, 即它实际只使用了 u2 的长度。

code 属性是 Class 文件中最重要的一个属性, 用于描述代码。

exception_table 异常表 (此处的异常表不是上面的 Exception) 对于 Code 属性来说并不是必须存在的, 如果出现的话, 它的结构如下

名称 类型 数量
start_pc u2 1
end_pc u2 1
handler_pc u2 1
catch_type u2 1

第 start_pc 行开始 到 end_pc (不包含 end_pc) 行出现了 catch_type 类型或其子类的异常, 跳转到 handler_pc 行继续执行,
catch_type 为 0, 则任意异常到需要到 handler_pc 行去处理。

  • Exception 属性

此处的异常不是上面的异常表。 Exceptions 属性的作用是列举出方法中可能抛出的受查异常 (Checked Exceptions), 也就是方法描述时在
throws 关键字后面列举的异常

Exception 属性结构

名称 类型 数量
attribute_name_index u2 1
attribute_length u4 1
number_of_exceptions u2 1
exception_index_table u2 1

number_of_exceptions 项表示方法可能抛出 number_of_exceptions 种受查异常, 每一种受查异常使用一个 exception_index_table
项表示; exception_index_table 是一个指向常量池中 CONSTANT_Class_info 型常量的索引。

至于属性表集合其他的属性, 这里就不一一列举了。
有兴趣可以阅读一下 周志明的《深入理解Java虚拟机》第 3 部分。

参考

《深入理解Java虚拟机》- 周志明
Java代码编译过程简述


  目录