虚拟机的存在使得 Java 实现了平台无关性,而字节码的存在使得 JVM 实现了语言无关性,从此 Java 只是 JVM 的一个用户而已。
- 本文所有信息基于作者实践验证,如有错误,恳请指正,不胜感谢。
- 转载请于文首标明出处:【Java】JVM - 字节码文件结构 (juejin.cn)
- 文章仍未完工,内容会逐步完善
所属专栏:JVM - 辐射工兵的专栏 - 掘金 (juejin.cn)
字节码文件
向后兼容性
Java 是一款向后兼容的编程语言,除了 Java 本身提供的设计支持之外,主要依靠的是字节码文件的向后兼容性。新版的字节码文件格式永远只在原有的结构上进行新增和扩充,不会对原定义的内容进行修改。
存储方式
字节码文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有任何分隔符。当遇到 8 个字节以上空间的数据项时,会以高位在前的方式(即大端存储)分割为若干个 8 个字节进行存储。
总体结构
字节码文件结构:
ClassFile {
u4 magic; // 魔数值 0xCAFEBABE
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]; // 属性表
}
可以看到,字节码文件格式采用一种类似于 C 语言结构体的伪结构来存储数据。同时所有数据的格式都是以 u_
或者 __info
的方式命名的。这代表了字节码文件结构中只定义了两种数据类型:“无符号数”和“表”。
-
无符号数包括
u1
、u2
、u4
和u8
,代表一到八个字节的无符号数,用于表示数值和字符,采用 UTF-8 编码来存储和表示字符。 -
表是一种复合结构类型,多个列表项组成一个表,列表项由无符号数和表组成,因此表是嵌套的数据结构。回头看,字节码文件也是一张表。每个表前面都会有一个
_count
字段用于描述表的长度,类似于 JVM 数组的对象头中有一个 length 属性用于描述数组长度。
字节码文件结构
前面部分定义了以下程序进行演示:
package com.test.jdk8;
public class HelloWorld {
private String helloString;
public void setHelloString(String helloString) {this.helloString = helloString;}
public void sayHello() {System.out.println(this.helloString);}
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.setHelloString("Hello World");
helloWorld.sayHello();
}
}
Magic
魔数值 magic
,用于表示当前文件为字节码文件,固定为 0xCAFEBABE
,占 4 个字节。
000000 ca fe ba be ...
版本号
版本号由 minor_version
和 major_version
组成,分别为次版本号和主版本号,都是 2 字节。虚拟机可以加载所有低于等于其版本号的字节码文件,以实现向后兼容。但是会拒绝加载所有高于其版本号的字节码文件。
次版本号除了 Java1.0+ 版本有使用之外,其他的版本基本上都是 0。主版本号从 45 开始,即 JDK1.0 主版本号就是 45。
000000 ca fe ba be 00 00 00 34 ....
// JDK8 生成的字节码文件是 52,即 0x0034
常量池
每个字节码文件中都有一个用于存储当前文件中所有常量的常量池,和 JVM 运行时数据结构中的字符串常量池是两个东西。
常量池大小
常量池大小 constant_pool_count
是一个 u2
的数值,因此常量池最多支持 个常量,后面字节码指令的参数大小设置与此相关。
000000 ca fe ba be 00 00 00 34 00 2e ...
// 0x002e = 46,表示本字节码文件常量池中有 46 个常量
常量池 constant_pool
是一个列表类型,也是从第 0 项开始使用的,但是常量池不会定义第 0 项,因为第 0 项规定用来表示:“不指向任何常量”。所以可以看到,constant_pool
后面接上的数量是 [constant_pool_count-1]
,但谨记,使用依旧是从第 0 项开始,只是没有具体定义而已。
常量池内容示例:
可以看到,确实从第 0 项开始,到 45 项常量截止,刚好是 46 项常量。
Classfile /C:/Workspace/Idea/java-test/out/production/java-test/com/test/jdk8/HelloWorld.class
...
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#31 // com/test/jdk8/HelloWorld.helloString:Ljava/lang/String;
#3 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #34.#35 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #36 // com/test/jdk8/HelloWorld
#6 = Methodref #5.#30 // com/test/jdk8/HelloWorld."<init>":()V
#7 = String #37 // Hello World
#8 = Methodref #5.#38 // com/test/jdk8/HelloWorld.setHelloString:(Ljava/lang/String;)V
#9 = Methodref #5.#39 // com/test/jdk8/HelloWorld.sayHello:()V
#10 = Class #40 // java/lang/Object
#11 = Utf8 helloString
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/test/jdk8/HelloWorld;
#20 = Utf8 setHelloString
#21 = Utf8 (Ljava/lang/String;)V
#22 = Utf8 sayHello
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 helloWorld
#28 = Utf8 SourceFile
#29 = Utf8 HelloWorld.java
#30 = NameAndType #13:#14 // "<init>":()V
#31 = NameAndType #11:#12 // helloString:Ljava/lang/String;
#32 = Class #41 // java/lang/System
#33 = NameAndType #42:#43 // out:Ljava/io/PrintStream;
#34 = Class #44 // java/io/PrintStream
#35 = NameAndType #45:#21 // println:(Ljava/lang/String;)V
#36 = Utf8 com/test/jdk8/HelloWorld
#37 = Utf8 Hello World
#38 = NameAndType #20:#21 // setHelloString:(Ljava/lang/String;)V
#39 = NameAndType #22:#14 // sayHello:()V
#40 = Utf8 java/lang/Object
#41 = Utf8 java/lang/System
#42 = Utf8 out
#43 = Utf8 Ljava/io/PrintStream;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
常量类型
从上文可以看到,每一项常量由常量类型和常量值组成,这个常量类型称为 tag
,而常量值的数据结构随常量类型的不同而不同。
| Constant Name | tag | Desc
|:---------------------------------|----:|:--------------------------------
| CONSTANT_Utf8_info | 1 | 一个 UTF8 格式的字符串值
| CONSTANT_Integer_info | 3 | 一个 int 值
| CONSTANT_Float_info | 4 | 一个 float 值
| CONSTANT_Long_info | 5 | 一个 long 值
| CONSTANT_Double_info | 6 | 一个 double 值
| 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_MehtodHandle_info | 15 | 一个方法句柄
| CONSTANT_MethodType_info | 16 | 一个方法类型
| CONSTANT_Dynamic_info | 17 | 一个动态计算的常量
| CONSTANT_InvokeDynamic_info | 18 | 一个动态调用点
| CONSTANT_Module_info | 19 | 一个模块
| CONSTANT_Package_info | 20 | 一个包
Utf8
CONSTANT_Utf8_info {
u1 tag; // 0x0001
u2 length; // 字符串的字节数
u1 bytes[length]; // 字节数组,字符串的 UTF8 字节表示
}
表示一个字符串的具体值,使用 UTF8 格式编码。由于 Utf8 字符串的 length 为 2 个字节长度,因此在 JVM 中,允许接受的字符串常量最长为 65535 个字节数。
000180 .. .. .. .. .. .. .. .. .. .. .. .. 01 00 0b 48 ...............H
000190 65 6c 6c 6f 20 57 6f 72 6c 64 .. .. .. .. .. .. ello World......
// tag : 0x01 = 1
// length : 0x000b = 11
// bytes : 0x(48 65 6c 6c 6f 20 57 6f 72 6c 64) = Hello World
Integer、Float、Long、Double
分别用于存储 int、float、long、double 常量。对于 byte、short、char、boolean 并没有专门的常量池类型用于存储,因此这四种类型全部以 CONSTANT_Integer_info 类型存储。
CONSTANT_Integer_info {
u1 tag; // 0x03 = 3
u4 bytes; // 具体值
}
CONSTANT_Float_info {
u1 tag; // 0x04 = 4
u4 bytes; // 具体值
}
CONSTANT_Long_info {
u1 tag; // 0x05 = 5
u4 high_bytes; // Long 的高 32 位
u4 low_bytes; // Long 的低 32 位
}
CONSTANT_Double_info {
u1 tag; // 0x06 = 6
u4 high_bytes; // Double 的高 32 位
u4 low_bytes; // Double 的低 32 位
}
String
CONSTANT_String_info {
u1 tag; // 0x08 = 8
u2 string_index; // UTF8 常量索引,即字符串的具体值
}
表示一个 String 常量,但是并没有实际的值,实际的值在 UTF8 类型常量中,因此 string_index
指向该实际字符串值。
000020 .. .. .. .. .. .. 08 00 25 .. .. .. .. .. .. .. $.......%....&..
// tag : 0x08 = 8
// string_index : 0x0025 = 37
// #37 : Utf8 Hello World
Class
CONSTANT_Class_info {
u1 tag; // 0x07 = 7
u2 name_index; // UTF8 常量索引,即符号引用的具体值
}
表示一个类,某个类的符号引用指的就是指向某个类的 CONSTANT_Class_info 常量。
000010 .. .. .. .. .. .. .. .. .. .. .. .. .. .. 07 00 ...... .!..".#..
000020 24 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. $.......%....&..
// tag : 0x07 = 7
// name_index : 0x0024 = 36
// #36 : Utf8 com/test/jdk8/HelloWorld
MethodAndType
CONSTANT_NameAndType_info {
u1 tag; // 0x0c = 12
u2 name_index; // UTF8 常量索引,即方法或字段简单名称的具体值
u2 descriptor_index; // UTF8 常量索引,即方法或字段描述符的具体值
}
表示一个 NameAndType 类型的常量,Name 是指方法或字段的简单名称,Type 是指方法或字段的描述符。
000160 .. .. .. .. .. .. .. .. .. .. .. .. 0c 00 2d 00 ...)..*.+..,..-.
000170 15 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ....com/test/jdk
// tag : 0x0c = 12
// name_index : 0x002d = 45
// descriptor_index : 0x0015 = 21
// #45 : Utf8 println
// #21 : Utf8 (Ljava/lang/String;)V
FieldRef、MethodRef、InterfaceMethodRef
CONSTANT_Fieldref_info {
u1 tag; // 0x09 = 9
u2 class_index; // Class 常量索引,指向所属类的符号引用
u2 name_and_type_index; // NameAndType 常量索引,指向字段的名称和描述符
}
CONSTANT_Methodref_info {
u1 tag; // 0x0a = 10
u2 class_index; // Class 常量索引,指向所属类的符号引用
u2 name_and_type_index; // NameAndType 常量索引,指向方法的名称和描述符
}
CONSTANT_InterfaceMethodref_info {
u1 tag; // 0x0b = 11
u2 class_index; // Class 常量索引,指向所属接口的符号引用
u2 name_and_type_index; // NameAndType 常量索引,指向方法的名称和描述符
}
与类的符号引用类似,这三者分别表示字段的符号引用、方法的符号引用和接口方法的符号引用。三者的数据结构是一样的,class_index
指向所属类的符号引用,name_and_type_index
指向 NameAndType 常量,目标常量包含着当前字段/方法的简单名称和描述符,描述符就是类型信息。
000000 ca fe ba be .. .. .. .. .. .. .. .. .. .. .. 09 .......4........
000010 00 05 00 1f .. .. .. .. .. .. .. .. .. .. .. .. ...... .!..".#..
// tag : 0x09 = 9
// class_index : 0x0005 = 5
// name_and_type_index : 0x001f = 31
// #5 : Class #36 // com/test/jdk8/HelloWorld
// #31 : NameAndType #11:#12 // helloString:Ljava/lang/String;
-
一切在类中定义的方法,无论是静态、实例还是抽象方法,其符号引用都是
MethodRef
类型。 -
一切在接口中定义的方法,无论是静态还是实例方法,其符号引用都是
InterfaceMethodRef
类型。 -
一切在类中定义的变量,无论是静态变量还是实例变量,其符号引用都是
FieldRef
类型。 -
一切在接口中定义的静态常量,都不会称为实现类真正的数据域,会在编译过程中提取为字面量直接注入调用代码中,因此请勿将接口作为常量类使用。
public class Tmp { public void foo() {System.out.println(MyInterface.staticInterfaceField);} public static interface MyInterface { String staticInterfaceField = "GOOD"; } } // 编译为 class 文件再反编译后的代码: public class Tmp { public Tmp() {} public void foo() {System.out.println("GOOD");} public interface MyInterface { String staticInterfaceField = "GOOD"; } }
MethodHandle
CONSTANT_MethodHandle_info {
u1 tag; // 0x0f = 15
u1 reference_kind; // 枚举值,代表方法句柄的类型
u2 reference_index; // FieldRef、MethodRef 或 InterfaceMethodRef 常量索引,指向字段或者方法的符号引用
}
代表一个方法句柄,方法句柄相当于“一个可调用的方法或字段 + 调用方式”,通过方法句柄就可以直接调用一个方法或者字段。reference_kind
就是调用方式,reference_index
就是方法的符号引用。调用方式为:
| Kind | Description | reference_index_type | Interpretation |
|:----:|:---------------------|:-----------------------------|:-----------------------------------------|
| 1 | REF_getField | FieldRef | getfield C.f:T |
| 2 | REF_getStatic | FieldRef | getstatic C.f:T |
| 3 | REF_putField | FieldRef | putfield C.f:T |
| 4 | REF_putStatic | FieldRef | putstatic C.f:T |
| 5 | REF_invokeVirtual | MethodRef | invokevirtual C.m:(A*)T |
| 6 | REF_invokeStatic | MethodRef/InterfaceMethodRef | invokestatic C.m:(A*)T |
| 7 | REF_invokeSpecial | MethodRef/InterfaceMethodRef | invokespecial C.m:(A*)T |
| 8 | REF_newInvokeSpecial | MethodRef | new C; dup; invokespecial C.<init>:(A*)V |
| 9 | REF_invokeInterface | InterfaceMethodRef | invokeinterface C.m:(A*)T |
MethodType
CONSTANT_MethodType_info {
u1 tag; // 0x0c = 12
u2 descriptor_index; // Utf8 常量索引,指向方法描述符的具体值
}
表示一个方法的类型。
InvokeDynamic
CONSTANT_InvokeDynamic_info {
u1 tag; // 0x12 = 18
u2 bootstrap_method_attr_index; // 引导方法表的索引,指向动态调用点的引导方法
u2 name_and_type_index; // NameAndType 常量索引,指向方法的符号引用
}
表示一个动态调用点(Dynamic CallSite)。bootstrap_method_attr_index
指向引导方法,引导方法定义在引导方法表中,引导方法表是 Class 文件的属性之一。当 Class 文件存在 InvokeDynamic 常量时,Class 文件必须包含它指向的引导方法,否则无法通过校验。
其他
-
Dynamic
(u2 bootstrap_mothod_index, u2 name_and_type_index)
:表示一个动态计算的常量,bootstrap_mothod_index 指向引导方法表中的方法。name_and_type_index 指向 NameAndType 常量,表示当前常量的名称和描述符。
-
Module
(u2 utf8_index)
:表示一个模块,utf8_index 指向模块名字符串。
-
Package
(u2 utf8_index)
:表示一个包名,utf8_index 指向包名字符串。
访问标志
访问标志 access_flags
由两个字节组成,共 16 个 bit,每个位代表了一个标志,通过或运算进行标志组合。
| Flag | bit | Hex | Desc |
|:---------------|:-------------------:|:------:|:----------------------|
| ACC_PUBLIC | ----_---- ----_---1 | 0x0001 | is Public |
| ACC_FINAL | ----_---- ---1_---- | 0x0010 | is Final |
| ACC_SUPER | ----_---- --1-_---- | 0x0020 | (always true) |
| ACC_INTERFACE | ----_--1- ----_---- | 0x0200 | is an interface |
| ACC_ABSTRACT | ----_-1-- ----_---- | 0x0400 | is abstract |
| ACC_SYNTHETIC | ---1_---- ----_---- | 0x1000 | is not define by code |
| ACC_ANNOTATION | --1-_---- ----_---- | 0x2000 | is an annotation |
| ACC_ENUM | -1--_---- ----_---- | 0x4000 | is an enum |
| ACC_MUDULE | 1---_---- ----_---- | 0x8000 | is a module |
类索引
类索引 this_class
由两个字节组成,指向常量池中类类型常量,即父类的全限定名。
父类索引
父类索引 super_class
由两个字节组成,指向常量池中类类型常量,即父类的全限定名。
接口索引
接口索引信息由接口数 interface_count
和 interfaces
接口列表组成。接口列表的列表项大小为两个字节,指向常量池中类类型常量,即实现接口的全限定名。
000200 .. .. .. .. .. .. .. .. 00 21 00 05 00 0a 00 00 .println.!......
// 00 21 是类的访问标志,激活 ACC_PUBLIC 和 ACC_SUPER 属性。
// 00 05 是本类索引,指向 #5 = Class #36 // com/test/jdk8/HelloWorld
// 00 0a 是父类索引,指向 #10 = Class #40 // java/lang/Object
// 00 00 是接口数量,为 0
字段表
字段表由字段数量 fields_count
和字段列表 fields
组成。fields_count
同样由两个字节组成,由此可见一个类至多有 65535 个字段。
字段结构
字段表的成员结构如下,每个成员表示一个字段:
field_info {
u2 access_flags; // 访问标志
u2 name_index; // 名称索引
u2 descriptor_index; // 描述符索引
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 属性列表
}
name_index
指向常量池中的 UTF8 常量类型,是字段名,descriptor_index
指向常量池中的 UTF8 常量类型,是字段描述符。
描述符索引之后是属性表,属性表与整个字节码文件的属性表类似,用于描述一些额外的属性。
TODO:为什么不直接指向 NameAndType 类型呢?是因为 NameAndType 类型出现得比较晚吗?还是不想二次寻找,不过消耗也不大呀。
字段访问标志
字段访问标志与类访问标志设计理念相同:
| Flag | bit | Hex | Desc |
|:--------------|:-------------------:|:------:|:-------------|
| ACC_PUBLIC | ----_---- ----_---1 | 0x0001 | is Public |
| ACC_PRIVATE | ----_---- ----_--1- | 0x0002 | is Private |
| ACC_PROTECTED | ----_---- ----_-1-- | 0x0004 | is Protected |
| ACC_STATIC | ----_---- ----_1--- | 0x0008 | is Static |
| ACC_FINAL | ----_---- ---1_---- | 0x0010 | is Final |
由于示例类只有一个字段,因此字段表的 fields_count = 0x01,0x02 标识该字段为 Private 属性。后面的 name_index 和 descriptor_index 分别指向常量池中的 Utf8 常量,标识该字段完整信息为 Ljava/lang/String helloString
。后面 0x00 表示属性表长度为 0。
000210 00 01 00 02 00 0b 00 0c 00 00 .. .. .. .. .. .. ................
// 0x0b = #11 = Utf8 helloString
// 0x0c = #12 = Utf8 Ljava/lang/String;
方法表
方法表由方法数量 methods_count
和方法列表 methods
组成,methods_count
同样由两个字节组成,代表一个类中最多存在 65535 个方法。
000210 .. .. .. .. .. .. .. .. .. .. 00 04 00 01 00 0d ................
// 04 表示有四个方法,后面开始就是方法的信息
方法表结构
方法表的成员结构如下,每一个成员表示类中的一个方法:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
name_index
指向方法名常量,descriptor_index
指向方法描述符常量,后面同样接属性表。方法的代码存储与方法属性表中的 Code 属性中。
方法访问标志
| Flag | bit | Hex | Desc |
|:-----------------|:-------------------:|:------:|:---------------------------------|
| ACC_PUBLIC | ----_---- ----_---1 | 0x0001 | is Public |
| ACC_PRIVATE | ----_---- ----_--1- | 0x0002 | is Private |
| ACC_PROTECTED | ----_---- ----_-1-- | 0x0004 | is Protected |
| ACC_STATIC | ----_---- ----_1--- | 0x0008 | is Static |
| ACC_FINAL | ----_---- ---1_---- | 0x0010 | is Final |
| ACC_SYNCHRONIZED | ----_---- --1-_---- | 0x0020 | is Synchronized |
| ACC_BRIDGE | ----_---- -1--_---- | 0x0040 | is create by compiler for bridge |
| ACC_VARARGS | ----_---- 1---_---- | 0x0080 | accept variable args |
| ACC_NATIVE | ----_---1 ----_---- | 0x0100 | is Native |
| ACC_ABSTRACT | ----_-1-- ----_---- | 0x0400 | is Abstract |
| ACC_STRICT | ----_1--- ----_---- | 0x0800 | is strictfp |
| ACC_SYNTHETIC | ---1_---- ----_---- | 0x1000 | is create by compiler |
属性表
无论是类、方法还是字段,都存在自己的属性表,任何人都能够自定义编译方法并向属性表添加自定义的属性,但是 JVM 运行时会忽略无法识别的属性。属性表中每个属性都是一个数据结构。所有属性共同要遵守的结构是:
attribute_info {
u2 attribute_name_index; // 属性名称
u4 attribute_length; // 属性长度
// custom fields...
}
每个属性都必须在前两位标识自己的属性名称,因此 attribute_name_index 指向自己的属性名常量,attribute_length 为 4 个字节的数值,表示当前属性的长度。由于所有属性都必须有名称和长度字段,且固定为 6 个字节,因此属性长度固定为整个属性项目长度减去 6。
所有属性
暂时记录的是 JDK8 的所有属性,其他的有时间再补上。
| Attribute | Scope | Clas File | JDK |
|:-------------------------------------|:-----------------------------------------|:---------:|:-----:|
| SourceFile | ClassFile | 45.3 | 1.0.2 |
| InnerClasses | ClassFile | 45.3 | 1.1 |
| EnclosingMethod | ClassFile | 49.0 | 5 |
| SourceDebugExtension | ClassFile | 49.0 | 5 |
| BootstrapMethods | ClassFile | 51.0 | 7 |
| ConstantValue | field_info | 45.3 | 1.0.2 |
| Code | method_info | 45.3 | 1.0.2 |
| Exceptions | method_info | 45.3 | 1.0.2 |
| RuntimeVisibleParameterAnnotations | method_info | 49.0 | 5 |
| RuntimeInvisibleParameterAnnotations | method_info | 49.0 | 5 |
| AnnotationDefault | method_info | 49.0 | 5 |
| MethodParameters | method_info | 52.0 | 8 |
| Synthetic | ClassFile, field_info, method_info | 45.3 | 1.1 |
| Deprecated | ClassFile, field_info, method_info | 45.3 | 1.1 |
| Signature | ClassFile, field_info, method_info | 49.0 | 5 |
| RuntimeVisibleAnnotations | ClassFile, field_info, method_info | 49.0 | 5 |
| RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | 49.0 | 5 |
| LineNumberTable | Code | 45.3 | 1.0.2 |
| LocalVariableTable | Code | 45.3 | 1.0.2 |
| LocalVariableTypeTable | Code | 49.0 | 5 |
| StackMapTable | Code | 50.0 | 6 |
| RuntimeVisibleTypeAnnotations | ClassFile, field_info, method_info, Code | 52.0 | 8 |
| RuntimeInvisibleTypeAnnotations | ClassFile, field_info, method_info, Code | 52.0 | 8 |
对 JVM 至关重要的属性
ConstantValue
// available in field_info
ConstantValue_attribute {
u2 attribute_name_index; // ConstantValue
u4 attribute_length;
u2 constantvalue_index; // 常量索引
}
ConstantValue 用于通知虚拟机为静态变量赋值,目标值就是 constantvalue_index 所指向的常量池常量,因此只支持基本数据类型和字符串。对于类变量的赋值,有两种方式:
- 在类构造器
<clinit>()
中赋值。 - 使用 ConstantValue 属性。
Oracle 官方实现的编译器是这么玩的:当静态变量是 final
属性并且为基础数据类型或者字符串时,才使用 ConstantValue 属性。虽然很有道理,但是 JVM 并没有要求一定要 final 域才能用 ConstantValue 属性。
对于 short、byte、char、boolean 这几种基本数据类型的常量,上文已经提及了,都以 int 常量进行保存。
Code
// available in method_info
Code_attribute {
u2 attribute_name_index; // Code
u4 attribute_length;
u2 max_stack; // 最大操作数栈深度
u2 max_locals; // 最大本地变量表长度,是变量槽的数量
u4 code_length; // 字节码长度
u1 code[code_length]; // 字节码列表
u2 exception_table_length; // 异常表长度
{ u2 start_pc; // catch 起始字节码指令偏移量
u2 end_pc; // catch 结束字节码指令偏移量
u2 handler_pc; // 异常处理程序起始字节码偏移量
u2 catch_type; // catch 异常类型
} exception_table[exception_table_length]; // 异常表
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 属性表
}
JVM 运行方法时根据 max_stack
分配当前栈帧的操作数栈深度,由于编译时可以计算出来,因此编译时会计算写入。
JVM 运行方法时根据 max_locals
分配本地变量表的变量槽数量,由于存在变量槽复用的需求,编译器在编译期间是根据变量的作用域来分配变量槽的,因此变量槽的数量一般小于定义的所有变量所需的变量槽数。方法接收者(即 this
指针)、方法参数、局部变量和显式异常处理的参数(catch 的异常)都需要本地变量表存储。
code_length
定义了 code
的长度,code
中不仅仅只有字节码指令,还有指令对应的参数。每个指令都是以一个 u1
的指令序号存储,JVM 获取该指令序号后,会解析出指令所需的参数,再来解析后面的字节。
由于指令序号使用一个字节存储,那我们可以知道字节码指令最多有 256 条。
虽然code_length
是四个字节数据,但是 JVM 规定一个方法最多只能有 65535 条字节码指令,但加上指令参数之后,所需的长度已经超u2
了,所以使用u4
存储 code 长度。
exception_table
是方法的显示处理异常表,我们显式处理的异常都会记录在这里。每一个异常处理表项目都确定了一个特定异常的处理方式,具体作用见注释。
如果定义了 finally 代码块,对于未捕获的异常,会在异常捕获程序的后面生成一个对所有异常的捕获程序,里面的代码就只是 finally 中定义的代码,最后接一个 throw
指令将异常抛出。如以下示例:
public static int foo(int x) {
try {
x = 10 / x;
} catch (Exception e) { // 异常处理程序捕获 x = 10 / x 执行的异常
x = 2;
return x;
} finally { // 无论是否出现异常,都进行 x++
x++;
}
return x;
}
可以看到,在定义异常处理程序之后,如果定义了 finally 块,会在所有可能执行的代码分支后面加上 finally 程序代码。同时,会对 try 块和 catch 块代码进行所有异常 any
的捕获,并在捕获之后执行 finally 逻辑。
因此,如果在 finally 中定义 return,执行的必然是 finally 块中的 return 指令。因为所有 finally 代码会插入到所有可能的分支结尾,且在原 return 语句之前。
0: bipush 10 // 异常处理程序起始捕获偏移量
2: iload_0 //
3: idiv //
4: istore_0 //
5: iinc 0, 1 // 异常处理程序结束捕获偏移量,不包含此偏移量本身;同时是 finally 代码块
8: goto 27 // 如果没发生异常,跳过异常处理程序
11: astore_1 // 异常处理程序起始偏移量
12: iconst_2 //
13: istore_0 //
14: iload_0 //
* 15: istore_2 // 将要 return 的值保存到变量槽 2 中
16: iinc 0, 1 // finally 代码块
* 19: iload_2 // 从变量槽 2 读取保存的值
* 20: ireturn // 异常处理程序结束偏移量,返回读取到的变量。
21: astore_3 //
22: iinc 0, 1 // finally 代码块
25: aload_3 //
26: athrow // 对于未定义的异常,直接抛出
27: iload_0 //
28: ireturn // 正常返回
Exception table:
from to target type
0 5 11 Class java/lang/Exception // 自定义异常捕获
0 5 21 any // 捕获未定义的异常
11 16 21 any // 捕获异常处理程序的异常
同时,注意 15、19 和 20 号偏移量,可以看出,return 的值 x
在 finally 块执行前已经是确定的了,并且存储在变量槽 2 中。即使在 finally 代码块中对 x
进行修改,只会影响变量槽 0 中的值,并不会影响到返回的值。这是一个比较细节重要的点:return 前执行 finally,在 finally 块中修改 return 的变量,并不会造成影响。
StackMapTable
// available for Code
StackMapTable {
u2 attribute_name_index; // StackMapTable
u4 attribute_length;
u2 number_of_entries; // entry 数量
stack_map_frame entries[number_of_entries]; // 栈映射帧,用于类型检查验证
}
对于不小于 50.0 版本号的 class 文件,如果没有 StackMapTable,JVM 会认定 class 文件具有一个隐式的 StackMapTable,即 0 个 entry。
这个属性会在 JVM 类加载的验证阶段被新的类型检查验证器使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。新的类型检查通过在编译期将一系列的验证类型直接记录在 StackMapTable 中,实现了大幅提升字节码验证的性能,实际上就是通过预处理的方式加快验证速度。见 Chapter 4. The class File Format (oracle.com)
Exceptions
// available for method_info
Exceptions {
u2 attribute_name_index; // Exceptions
u4 attribute_length;
u2 number_of_exceptions; // 异常数量
u2 exception_index_table[number_of_exceptions]; // 异常表,每个异常项是常量池中 Class 常量的索引
}
Exceptions 与 Code 里面的 exception_table
属性不一样,前者是受检异常表,用于列举方法抛出的受检异常,即我们定义在 throws
关键字后的异常;后者定义了方法中异常的捕获与处理程序,是一段代码。
exception_index_table
中每个列表项都是常量池索引,因此是 u2
数据类型,指向常量池中代表异常的 Class 常量。
BootstrapMethods
// available for ClassFile
BootstrapMethods {
u2 attribute_name_index; // BootstrapMethods
u4 attribute_length;
u2 num_bootstrap_methods; // 引导方法的数量
{ u2 bootstrap_method_ref; // 【列表项】常量池中 MethodHandle 类型常量的索引
u2 num_bootstrap_arguments; // 【列表项】bootstrap_arguments 数量
u2 bootstrap_arguments[num_bootstrap_arguments]; // 【列表项】参数列表,里面的列表项是常量池常量的索引
} bootstrap_methods[num_bootstrap_methods]; // 引导方法列表
}
于 JDK7 时增加,定义了类中的引导方法。如果一个类中存在 Dynamic 或者 InvokeDynamic 类型的常量,则类中必须存在 BootstrapMethod 属性,因为是引导方法的编号是它们的参数。
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#29 // #0:applyAsInt:
...
#26 = MethodType #35 // (Ljava/lang/Object;)I
#27 = MethodHandle #6:#36 // invokestatic java/lang/Integer.parseInt:(Ljava/lang/String;)I
#28 = MethodType #14 // (Ljava/lang/String;)I
...
{
public int foo(java.lang.String);
Code:
stack=2, locals=3, args_size=2
0: invokedynamic #2, 0 // InvokeDynamic #0:applyAsInt:()Ljava/util/function/ToIntFunction;
5: astore_2
6: aload_2
7: aload_1
8: invokeinterface #3, 2 // InterfaceMethod java/util/function/ToIntFunction.applyAsInt:(Ljava/lang/Object;)I
13: ireturn
}
...
BootstrapMethods:
0: #25 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#26 (Ljava/lang/Object;)I
#27 invokestatic java/lang/Integer.parseInt:(Ljava/lang/String;)I
#28 (Ljava/lang/String;)I
可以看到,bootstrap_arguments
里的数据就是示例中 Method argument
中的数据,包括 MethodType 和 MethodHandle,实际上还支持 Class、String、Integer、Float、Long 和 Double 类型。
虽然 JDK7 时已经支持动态调用,但是当时很多编译器不支持生成 InvokeDynamic 常量和 BootstrapMethods,所以当时的 Java 版本基本没有使用到动态调用功能,等到 JDK8 时 Java 语言支持 Lambda 表达式之后,动态调用才逐渐有了用武之地。
对 Java 的重要属性
InnerClasses
// available for ClassFile
InnerClasses {
u2 attribute_name_index; // InnerClasses
u4 attribute_length;
u2 number_of_classes; // 内部类数量
{ u2 inner_class_info_index; // 内部类的符号引用,指向常量池 Class 常量
u2 outer_class_info_index; // 宿主类的符号引用,指向常量池 Class 常量
u2 inner_name_index; // 内部类的名称,指向常量池 Utf8 常量
u2 inner_class_access_flags; // 内部类的访问属性
} classes[number_of_classes];
}
inner_name_index
是内部类的名称,如果是匿名内部类,为 0。inner_class_access_flags
访问属性:
| Flag | bit | Hex | Desc |
|:---------------|:-------------------:|:------:|:----------------------|
| ACC_PUBLIC | ----_---- ----_---1 | 0x0001 | is Public |
| ACC_PRIVATE | ----_---- ----_--1- | 0x0002 | is Private |
| ACC_PROTECTED | ----_---- ----_-1-- | 0x0004 | is Protected |
| ACC_STATIC | ----_---- ----_1--- | 0x0008 | is Static |
| ACC_FINAL | ----_---- ---1_---- | 0x0010 | is Final |
| ACC_INTERFACE | ----_---- --1-_---- | 0x0020 | is an interface |
| ACC_ABSTRACT | ----_-1-- ----_---- | 0x0400 | is abstract |
| ACC_SYNTHETIC | ---1_---- ----_---- | 0x1000 | is not define by code |
| ACC_ANNOTATION | --1-_---- ----_---- | 0x2000 | is an annotation |
| ACC_ENUM | -1--_---- ----_---- | 0x4000 | is an enum |
示例:
public class TestEnclosureForMethod {
// 使用局部类,捆绑 localClass 方法中的 tmpMsg,将局部变量 tmpMsg 逃逸出去。
public InnerClass localClass() {
String tmpMsg = "This is localClass() Method";
class TmpClass extends InnerClass {
@Override
public String getMsg() {return tmpMsg;}
}
return new TmpClass();
}
// 使用匿名类,捆绑 anonymousClass 方法中的 tmpMsg,将局部变量 tmpMsg 逃逸出去。
public InnerClass anonymousClass() {
String tmpMsg = "This is anonymousClass() Method";
return new InnerClass() {
@Override
public String getMsg() {return tmpMsg;}
};
}
// 内部类也可以用作闭包将外围类的变量逃逸出去,但此处不作演示
public static class InnerClass {
public String getMsg( ) {return "This is InnerClass Class";}
}
}
----------------------------------------------------------------------
Constant pool:
...
#3 = Class #30 // com/test/jdk8/closure/TestEnclosureForMethod$1TmpClass
#6 = Class #33 // com/test/jdk8/closure/TestEnclosureForMethod$1
#8 = Class #34 // com/test/jdk8/closure/TestEnclosureForMethod
#10 = Class #36 // com/test/jdk8/closure/TestEnclosureForMethod$InnerClass
#11 = Utf8 InnerClass
#13 = Utf8 TmpClass
...
InnerClasses:
// 内部类 InnerClass,拥有类名称(#11)、符号引用(#10)、宿主类符号引用(#8)
public static #11= #10 of #8; // InnerClass=class com/test/jdk8/closure/TestEnclosureForMethod$InnerClass of class com/test/jdk8/closure/TestEnclosureForMethod
// 匿名类,anonymousClass() 中创造的,只有符号引用(#6)。
#6; // class com/test/jdk8/closure/TestEnclosureForMethod$1
// 局部类,localClass() 中创造的,拥有类名称(#13)和符号引用(#3)。
#13= #3; // TmpClass=class com/test/jdk8/closure/TestEnclosureForMethod$1TmpClass
可以看到:
- 匿名类只有一个指向 Class 常量的自身符号引用,没有其他信息。连名字都没有,名副其实的匿名。且该符号引用是按照
$1
、$2
... 这样顺序生成的。 - 局部类拥有名称、自身符号引用,但是没有宿主类信息。且符号引用是
$1 + ClassName
、$2 + ClassName
... 这样顺序生成的,因为同一个临时类名可能会定义多次。 - 内部类拥有名称、自身符号引用和宿主类符号引用。
EnclosingMethod
// available for ClassFile
EnclosingMethod {
u2 attribute_name_index; // EnclosingMethod
u4 attribute_length;
u2 class_index; // 外围类的符号引用
u2 method_index; // 闭包的外围方法的符号引用
}
EnclosingMethod 用于表示一个类是当前类是一个局部类或者是匿名类,代表了本类的主要作用就是一个闭包。虽然内部类也有闭包的功能,但是局部类和匿名类可以捆绑方法中的变量,内部类只能捆绑外围类的变量。class_index
指向创建当前类的外围类,称为 EnclosingType。method_index
指向创建当前类的外围方法,称为 EnclosingMethod,如果当前闭包不是在方法或者构造方法中被创建的,则为 0。
public class Soldier {
public String getMsg() {return "Unknown";}
}
public class TestEnclosureForMethod {
private String secret = "";
public Soldier[] soldiers = new Soldier[1000];
public void updateSecret(String newSecret) {
this.secret = newSecret;
// 此时出现一个叛徒,成功窃取秘密
Soldier traitor = new Soldier() {
@Override
public String getMsg() {return newSecret;}
};
soldiers[13] = traitor;
}
}
------------------------------
class com.test.jdk8.closure.TestEnclosureForMethod$1com.test.jdk8.closure.TestEnclosureForMethod.TestEnclosureForMethod$1
Minor version: 0
Major version: 52
Flags: SUPER
SourceFile: TestEnclosureForMethod.java
EnclosingType: com/test/jdk8/closure/TestEnclosureForMethod
EnclosingMethod: com/test/jdk8/closure/TestEnclosureForMethod.updateSecret:(Ljava/lang/String;)V
InnerClasses:
com/test/jdk8/closure/TestEnclosureForMethod$1
如果不是在方法中或者构造方法中定义的闭包类型,则不会有 EnclosingMethod 属性。