目标:通过阅读 深入理解 JVM 虚拟机 第三版 的第 6 章,结合 ASM 里的 Reader 和 Visitor 对 class 文件有个一比较深入的了解。
6.2 无关性的基石 Java 虚拟机不与包括 Java 语言在内的任何语言绑定,它只与 Class 文件这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集,符号表以及若干其他辅助信息。
6.3 Class 类文件的结构
Idea 安装 BinEd 插件可以查看 Class 文件在各种进制下的值
Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class 文件结构中只有两种数据类型:无符号数 + 表
无符号数:是基本数据类,以 u1, u2, u4, u8 分别表示 1,2,4,8 个字节的无符号数。无符号数可以用来描述数字,索引引用,数量值或者按照 UTF-8 编码构成的字符串值
表:n 个无符号数 + 其他表构成的复合数据类型,命名习惯性的以 _info 结尾。表用于描述有层次关系的复合结构数据,整个 Class 可以看作一张表。
Class 文件格式表:
Type
Name
Count
u4
magic
1
u2
minor_version
1
u2
major_version
1
u2
constant_pool_count
1
cp_info
constant_pool
constant_pool_count-1
u2
access_flags
1
u2
this_class
1
u2
super_class
1
u2
interfaces_count
1
u2
interfaces
interfaces_count
u2
fields_count
1
field_info
fields
fields_count
u2
methods_count
1
method_info
methods
methods_count
u2
attributes_count
1
attribute_info
attributes
attributes_count
6.3.1 魔数与 Class 文件的版本 Class 文件前 4 个字节被称为魔数,值为 0xCAFEBABE,用来表明文件类型。紧接着 4 个字节为主次版本号。
Java 虚拟机规范在 Class 文件校验部分明确要求,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行 超过其版本号的 Class 文件。
JDK version
version number
JDK 13
57
JDK 12
56
JDK 11
55
JDK 10
54
JDK 9
53
JDK 8
52
JDK 7
51
JDK 6.0
50
JDK 5.0
49
JDK 1.4
48
JDK 1.3
47
JDK 1.2
46
JDK 1.1
45
仿照参考书写下测试代码, 不知道是不是编译器版本不一样,结果从常量池开始有些许偏差,不过无伤大雅,学习路径,方法还是一样的。
1 2 3 4 5 6 7 8 9 package c631;public class TestClass { private int m; public int inc () { return m + 1 ; } }
Class 文件 16 进制表达式
line
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
hex value
0000000000
ca fe ba be 00 00 00 32 00 16 0a 00 04 00 12 09
…….2……..
0000000010
00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00
………….m..
0000000020
01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
.I……()
0000000030
56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
V…Code…LineN
0000000040
75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
umberTable…Loc
0000000050
61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
alVariableTable.
0000000060
00 04 74 68 69 73 01 00 10 4c 63 36 33 31 2f 54
..this…Lc631/T
0000000070
65 73 74 43 6c 61 73 73 3b 01 00 03 69 6e 63 01
estClass;…inc.
0000000080
00 03 28 29 49 01 00 0a 53 6f 75 72 63 65 46 69
..()I…SourceFi
0000000090
6c 65 01 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a
le…TestClass.j
00000000a0
61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 0e
ava………….
00000000b0
63 36 33 31 2f 54 65 73 74 43 6c 61 73 73 01 00
c631/TestClass..
00000000c0
10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63
.java/lang/Objec
00000000d0
74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00
t.!………….
00000000e0
06 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00
…………….
00000000f0
00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1
../……..*….
0000000100
00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 03
…………….
0000000110
00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d
…………….
0000000120
00 00 00 01 00 0e 00 0f 00 01 00 09 00 00 00 31
……………1
0000000130
00 02 00 01 00 00 00 07 2a b4 00 02 04 60 ac 00
……..*….`..
0000000140
00 00 02 00 0a 00 00 00 06 00 01 00 00 00 07 00
…………….
0000000150
0b 00 00 00 0c 00 01 00 00 00 07 00 0c 00 0d 00
…………….
0000000160
00 00 01 00 10 00 00 00 02 00 11
………..
魔数值 cafe, 版本号 00 00 00 32
转化后位 50 和我在 pom 指定的 1.6 版本 JDK 编译一致
6.3.2 常量池 第 9-8 个字节表示常量池。常量池是从 1 开始的。示例中对应的值位 00 16 - 22
表明常量池总共有 21 个值。
PS: 常量池的 0 位空余,是为了考虑特殊情况。当指向常量池的数据需要表达 不引用任何常量池中的项目
这样的意思时,可以将索引值设置位 0 表示。
常量池主要存放两大类的常量:字面量 Literal + 符号引用 Symbolic References。字面量接近于 Java 语言层面的常量概念,符号引用则属于编译原理的概念主要包括下面几类常量:
被模块导出或者开放的包 package
类和接口的全名限定 Fully Qualified Name
字段名称和描述符 Desciptor
方法名称和描述符
方法句柄和方法类型 Method Handle, Mehtod Type, Invoke Dynamic
动态调用点和动态常量 Dynamically-Computed Call Site, Dynamically-Computed Constant
Class 文件中没有类似 C 语言中的链接,只有当 Class 文件在虚拟机中加载后才能确定内存分布。
常量池中每一项都是一个表,到 JDK13 为止有 17 种表结构
Type
Flag
Desc
CONSTANT_Utf8_info
1
UTF-8 编码的字符串
CONSTANT_Integer_info
3
整型字面量
CONSTANT_Float_info
4
浮点型字面量
CONSTANT_Long_info
5
长整型型字面量
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
表示一个模块中开放或者导出的包
常量池第一项以 0a - 10
开头,查看上表得知为 CONSTANT_Methodref_info 类型的表,查询可知对应的表结构为
Const Name
Item
Length
desc
CONSTANT_Methodref_info
tag
u1
值为 10
-
index
u2
指向声明方法的类描述符 CONSTANT_Class_info 的索引项
-
index
u2
指向名称及类型描述符 CONSTANT_NameAndType 的索引项
所以第一个常量值总共 5 个字节组成 0a 00 04 00 12
,表示的是方法引用,类引用地址为 4,方法名称和类型地址为 18。
为了反向验证这样分析是否正确可以通过反编译命令 javap -verbose TestClass
查看 class 文件。
第一个常量值内容为 #1 = Methodref #4.#18 // java/lang/Object."<init>":()V
和分析结果一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 C:\Users\jack\Downloads\helloworld\understanding-the-jvm\c6-file-structure\target\classes\c631>javap -verbose TestClass 警告: 文件 .\TestClass.class 不包含类 TestClass Classfile /C:/Users/jack/Downloads/helloworld/understanding-the-jvm/c6-file-structure/target/classes/c631/TestClass.class Last modified 2020年11月19日; size 363 bytes MD5 checksum 16826804824a30e99e96960a47c3a47a Compiled from "TestClass.java" public class c631.TestClass minor version: 0 major version: 50 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #3 // c631/TestClass super_class: #4 // java/lang/Object interfaces: 0, fields: 1, methods: 2, attributes: 1 Constant pool: #1 = Methodref #4.#18 // java/lang/Object."<init>":()V #2 = Fieldref #3.#19 // c631/TestClass.m:I #3 = Class #20 // c631/TestClass #4 = Class #21 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lc631/TestClass; #14 = Utf8 inc #15 = Utf8 ()I #16 = Utf8 SourceFile #17 = Utf8 TestClass.java #18 = NameAndType #7:#8 // "<init>":()V #19 = NameAndType #5:#6 // m:I #20 = Utf8 c631/TestClass #21 = Utf8 java/lang/Object { public c631.TestClass(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lc631/TestClass; public int inc(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lc631/TestClass; } SourceFile: "TestClass.java"
第二个常量为 09 开头,查表可知为 field 的引用
Const Name
Item
Length
desc
CONSTANT_Fieldref_info
tag
u1
值为 9
-
index
u2
指向声明字段的类或接口类描述符 CONSTANT_Class_info 的索引项
-
index
u2
指向字段描述符 CONSTANT_NameAndType 的索引项
值为 09 00 03 00 13
对应 #2 = Fieldref #3.#19 // c631/TestClass.m:I
第三个常量为 07 开头,为 Class 常量表
Const Name
Item
Length
desc
CONSTANT_Class_info
tag
u1
值为 7
-
index
u2
指向全限定名常量的索引项
07 00 14
对应 #3 = Class #20 // c631/TestClass
第四个也是 07 开头
07 00 15
- #4 = Class #21 // java/lang/Object
第五个为 01 开头, 表示 Utf8 类型的常量
Const Name
Item
Type
desc
CONSTANT_Utf8_info
tag
u1
值为 1
-
index
u2
UTF-8 编码的字符串占用的字节数
-
bytes
u1
长度为 length 的 UTF-8 编码字符串
01 00 01 6d
, 占用字节数 1,内容为 6d 的 UTF 内容 m
,对应关系可以通过各种在线工具查看,很常用 #5 = Utf8 m
第六个常量 01 00 01 49
- #6 = Utf8 I
第七个常量 01 00 06 3c 69 6e 69 74 3e
占用字节数 6 个 - #7 = Utf8 <init>
第八个 01 00 03 28 29 56
- #8 = Utf8 ()V
第九个 01 00 04 43 6f 64 65
- #9 = Utf8 Code
第十个 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65
- #10 = Utf8 LineNumberTable
第十一个 01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65
- #11 = Utf8 LocalVariableTable
第十二个 01 00 04 74 68 69 73
- #12 = Utf8 this
第十三个 01 00 10 4c 63 36 33 31 2f 54 65 73 74 43 6c 61 73 73 3b
- #13 = Utf8 Lc631/TestClass;
第十四个 01 00 03 69 6e 63
- #14 = Utf8 inc
第十五个 01 00 03 28 29 49
- #15 = Utf8 ()I
第十六个 01 00 0a 53 6f 75 72 63 65 46 69 6c 65
- #16 = Utf8 SourceFile
第十七个 01 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61
- #17 = Utf8 TestClass.java
第十八个 0c
开头,为 NameAndType 类型
Const Name
Item
Length
desc
CONSTANT_NameAndType_info
tag
u1
值为 12
-
index
u2
指向该字段或方法名称 常量项的索引项
-
index
u2
指向该字段或方法描述符 常量项的索引项
0c 00 07 00 08
- #18 = NameAndType #7:#8 // "<init>":()V
第十九 0c 00 05 00 06
- #19 = NameAndType #5:#6 // m:I
第二十 01 00 0e 63 36 33 31 2f 54 65 73 74 43 6c 61 73 73
- #20 = Utf8 c631/TestClass
第二十一 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
- #21 = Utf8 java/lang/Object
到此为止,常量池分析完毕
6.3.3 访问标志 紧跟在常量池之后,由两个字节组成,有 16 个标志位,当前只定义了 9 种。
Name
flag value
含义
ACC_PUBLIC
0x0001
是否为 public 类型
ACC_FINAL
0x0010
是否为 final 类型, 只有类可设置
ACC_SUPER
0x0020
是否允许使用 invokespecial 字节码指定的新语义, invokespecial 语义在 JDK 1.0.2 发生过改变, 为了区别这条指令使用哪种语义, JDK 1.0.2 之后编译出来的类这个标志必须为真
ACC_INTERFACE
0x0200
是否是一个接口
ACC_ABSTRACT
0x0400
是否为 abstract 类型,对于接口或者抽象类来说,此标志必须为真,其他类型为假
ACC_SYNTHETIC
0x1000
表示这个类并非由用户代码产生
ACC_ANNOTATION
0x2000
标识这是一个注解
ACC_ENUM
0x4000
标识这是一个枚举
ACC_MODULE
0x8000
标识这是一个模块
示例种值为 00 21
即 0020 & 0001 所以是 public + super 类型
6.3.4 类索引,父索引和接口索引集合
类索引(this_class) - u2 类型数据
父索引(super_class) - u2 类型数据
接口索引集合(super_class) - u2 类型数据
这些所以确定类的继承关系,实例中数据 00 03 00 04 00 00
表示 类所以指向常量池第三个常量,父索引指向第四个常量,接口集合数量为 0
#3 = Class #20 // c631/TestClass
#4 = Class #21 // java/lang/Object
6.3.5 字段表集合 用来描述接口或类中声明的变量。这里的变量只包括类级 变量以及实例级 变量,不包含局部变量。
字段表结构
类型
名称
数量
u2
access_flags
1
u2
name_index
1
u2
descriptor_index
1
u2
attribute_count
1
attribute_info
attributes
attribute_count
字段修饰符 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_SYNTHTIC
0x0100
字段是否由编译器产生
ACC_ENUM
0x0400
字段是否 enum
作用域修饰符: public/private/protected
是否是类级字段:static
是否可变:final
是否强制主从内存读写:volatile
是否可序列化:transient
name_index 和 descriptor_index 都指向常量池引用,表示字段简单名称以及字段和方法描述符。
全名限定:用斜线分割的 路径+类名
简单名称:只有名字,没有路径信息
方法和字段描述符:参数列表+返回值类型,例如 ()V, (Lcom/lang/Object;)V
基本数据类型含义表
字符
含义
B
byte
C
char
D
double
F
float
I
int
J
long
S
short
Z
boolean
V
void
L
对象类型
表示数组类型时,每一维度将使用一个前置的 [
字符描述,比如 String[][] 表示为 [[Ljava/lang/String;
, 整形数组 int[] 表示为 [I
。
实例中对应的字段表集合内容为 00 01 00 02 00 05 00 06 00 00
, interface 之后紧接着为 fields_count 的表示位, 00 01
, 表示只有一个 field。
00 02
表示方位权限 private,00 05
表示名字指向常量池第五个常量 m
, 00 06
表示描述符指向第六个常量 I
,00 00
属性表个数位 0 个。
6.3.6 方法表集合 方法表和之前的属性表,class 表是一个套路的, 方法表结构如下
类型
名称
数量
u2
access_flags
1
u2
name_index
1
u2
descriptor_index
1
u2
attribute_count
1
attribute_info
attributes
attribute_count
方法表的 access_flag 相对 field 少了 volatile 和 trasient, 多了 synchronized, native, strictfp 和 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
字段是否由编译器自动产生
方法中的具体实现经过 javac 编译成字节码指令后存在属性表集合中一个名为 Code 的属性里面。
实例内容 00 02 00 01 00 07 00 08 00 01 00 09
00 02 - 有两个方法
00 01 - public 类型的方法
00 07 - name 指向常量池7 -
00 08 - 描述符指向8 - ()V
00 01 - 属性数量 1
00 09 - 属性表索引 9,指向 Code
方法签名:Java 语法中的方法签名可以从重载(Overload)理解。Java 中重载要求方法名一致,参数列表及参数类型不同。返回值并不在比较范围内。方法除了返回值不同的重载是会编译错误的。但是在字节码的语义中,只有返回值不同的重载是合法的。
6.3.7 属性表集合 属性表集合的限制比前面那些结构要宽松一些,对虚拟机不认识的属性,会自动跳过。到 java 12 一共有 29 种预定义的属性
属性名称
使用位置
含义
Code
方法表
Java代码编译成的自己吗指令
ConstantValue
字段表
由 final 关键字定义的常量值
Deprecated
类,方法,字段表
被声明为 deprecated 的方法和字段
Exceptions
方法表
方法抛出的异常列表
EnclosingMethod
类文件
仅当一个类为局部类或匿名类是才拥有这个属性,用于标识这个类所在的外围方法
InnerClasses
类文件
内部类列表
LineNumberTable
Code属性
Java 源码的行号与字节码指令的对应关系
LocalVariableTable
Code属性
方法的局部变量描述
StackMapTable
Code属性
JDK6 新增,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需的类型是否匹配
Signature
类,方法表和字段表
JDK5新增,用于支持泛型情况下的方法签名
SourceFile
类文件
记录源文件名称
SourceDebugExtension
类文件
JDK5新增,存储额外的调试信息
Synthetic
类,方法表,字段表
标识是否由编译器产生
LocalVariableTypeTable
类
JDK5新增,使用特征签名代替描述符,为了支持泛型
RuntimeVisibleAnnotations
类,方法表,字段表
JDK5新增,为动态注解提供支持
RuntimeInVisibleAnnotations
类,方法表,字段表
JDK5新增,为动态注解提供支持,标识不可见
RuntimeVisibleParameterAnnotations
方法表
JDK5新增,作用对象为方法参数
RuntimeInvisibleParameterAnnotations
方法表
JDK5新增,作用对象为方法参数
AnnotationDefault
方法表
JDK5新增,注解类元素默认值
BootstrapMethods
类文件
JDK7新增,保存 invokedynamic 指令引用的引导犯法限定符
RuntimeVisibleTypeAnnotations
类,方法表,字段表, Code属性
JDK8新增
RuntimeInvisibleTypeAnnotations
类,方法表,字段表, Code属性
JDK8新增
MethodParameters
方法表
JDK8新增
Module
类
JDK9新增
ModulePackages
类
JDK9新增
ModuleMainClass
类
JDK9新增
NestHost
类
JDK11新增
NestMembers
类
JDK11新增
属性表结构
类型
名称
数量
u2
attribute_name_index
1
u4
attribute_length
1
u1
info
attribute_length
attribute_name_index 指向常量池中的一个引用,属性值结构完全自定义,attribute_length 说明属性值所占的位数。
Code Java 方法体种的代码经过 javac 编译器处理之后都会转化为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合中,但并非所有方法表都必须存在这个属性,比如抽象方法或接口中就可以不存在 Code 属性。
Code 属性表的结构
类型
名称
数量
含义
u2
attribute_name_index
1
指向 CONSTANT_Utf8_info 常量的索引,为固定值 Code
u4
attribute_length
1
属性值长度
u2
max_stack
1
操作数栈深度的最大值
u2
max_locals
1
局部变量表存储空间,单位-变量槽(Slot)
u4
code_length
1
编译后字节码指令个数
u1
code
code_length
编译后字节码指令
u2
exception_table_length
1
-
exception_info
exception_table
exception_table_length
-
u2
attribute_count
1
-
attribute_info
attributes
attribute_count
-
对于 byte, char, float, int, short, boolean 和 returnAddress 等长度不超过 32 byte 的数据类型,每个局部变量占用一个变量槽,double, long 这两个 64 位的占两个槽。
同时生存的最大局部变量和类型计算出 max_locals
字节码指令长度 u1。u1 可以最多表达 255 个指令,现在大约已经定义了 200 条。
测试案例中 init 方法对应的 code 代码块为 00 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02
00 09
前面已经说过,指向固定的 Code 字符地址
00 00 00 31
属性表长度 3*16 + 1 = 49
00 01
栈深 1
00 01
本地变量表大小 1
00 00 00 05
code 长度 5
2a b7 00 01 b1
code 内容
2a
: aload_0 将第一个变量推送至栈顶
b7
invokespecial, 后面接一个 u2 类型引用数据,执行构造方法或 private 方法,或它的父类方法
00 01
方法引用,指向 init
b1
return 指令
对应的 javap 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 public c631.TestClass(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lc631/TestClass;
args_size=1
方法虽然没有参数,但是 Java 编译时会把 this 作为第一个默认参数塞入 code 代码块中。
00 00 00 02
异常表长度 0, 属性表长度 2
异常表结构
类型
名称
数量
u2
start_pc
1
u2
end_pc
1
u2
handler_pc
1
u2
catch_type
1
异常代码案例
1 2 3 4 5 6 7 8 9 10 11 12 public int inc () { int x; try { x = 1 ; return x; } catch (Exception e) { x = 2 ; return x; } finally { x = 3 ; } }
对应的 javap 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public int inc(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=5, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: istore_2 4: iconst_3 5: istore_1 6: iload_2 7: ireturn 8: astore_2 9: iconst_2 10: istore_1 11: iload_1 12: istore_3 13: iconst_3 14: istore_1 15: iload_3 16: ireturn 17: astore 4 19: iconst_3 20: istore_1 21: aload 4 23: athrow Exception table: from to target type 0 4 8 Class java/lang/Exception 0 4 17 any 8 13 17 any 17 19 17 any
和书上的结果略有差别,但基本一致
Exceptions 属性 和 Code 平级的概念,并不是上一章节里 Code 下面的 exception 表。这里表示的是方法可能抛出的异常,就是 throws 后面的那些东西。属性结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
number_of_exceptions
1
u2
exception_index_table
number_of_exceptions
number_of_exceptions: 可能抛出的受检测的异常类型 exception_index_table: 指向常量池中的 CONSTANT_Class_info 索引
LineNumberTable 属性 描述 Java 源码行号和字节码行号之间的对应关系。可以在编译时指定不生成行号,但是会影响异常信息显示和 debug, 表结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
line_number_table_length
1
line_number_info
line_number_table
line_number_table_length
line_number_info: 包含 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 Java 源码行号。
LocalVarableTable 及 LocalVarableTypeTable 属性 LocalVarableTable 描述局部变量表的变量与 Java 源码中定义的变量之间的关系。非必须,可以指定 javac 参数去除且不影响运行。但是去除后方法参数名称会变为类似 arg0, arg1 的表示,不方便,表结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
local_variable_table_length
1
local_variable_info
local_variable_table
local_variable_table_length
local_variable_info 代表栈帧与源码中局部变量的关联,结构如下:
type
name
count
u2
start_pc
1
u2
length
1
u2
name_index
1
u2
descriptor_index
1
u2
index
1
start_pc + length: 限定了局部变量的作用范围,即作用域
name_index + descriptor_index: 指向常量池中 CONSTANT_Utf8_info 类型索引
index: 栈帧局部变量槽位置,当数据类型为 64 位则占用 index 和 index+1 两个
LocalVarableTypeTable 是 JDK5 时为了支持泛型而引入的,基本功能和 LocalVarableTable 一样。
SourceFile 及 SourceDebugExtension 属性 SourceFile 记录生成 Class 文件的源码文件名称,可选,通常与类名同,特殊情况除外(如内部类)。表结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
sourcefile_index
1
sourcefile_index: 指向常量池中 CONSTANT_Utf8_info 型常量的索引,值问文件名。
SourceDebugExtension 是 JDK5 中加入的新特性,存储额外调试信息,支持类似 JSP 这种使用 Java 编译器但是语法不同的语言,类中最多只允许一个该属性。表结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
debug_extension[attribute_length]
1
ConstantValue 属性 ConstantValue 通知虚拟机自动为静态变量赋值。只有被 static 修饰的变量才能使用这个属性。虚拟机中对非 static 变量在 () 方法总进行,对于静态变量则有两种方式,一种是构造器 () 另一种是 ConstantValue。Oracle 的 javac 中的实现方式为:static + final + 基本类型/String 在 ConstantValue 中赋值, 没有 final 或者是其他数据类型则在 () 中赋值。表结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
constantvalue_index
1
constantvalue_index: 指向常量池中一个引用,可选类型有 CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_Integer_info 和 CONSTANT_String_info。
InnerClasses 属性 InnerClasses 记录内部类与宿主类之间的关联。结构如下:
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
number_of_classes
1
inner_classes_info
inner_classes
number_of_classes
number_of_classes: 内部类个数
inner_classes_info 结构如下
type
name
count
u2
inner_class_info_index
1
u2
outer_class_info_index
1
u2
inner_name_index
1
u2
inner_class_access_flags
1
inner_class_info_index, outer_class_info_index:指向常量池中 CONSTANT_Class_info 常量索引,分别代表内部类和宿主类
inner_name_index:指向常量池 CONSTANT_Utf8_info 引用,代表内部类名称,如果是匿名内部类,值为 0
inner_class_access_flags:和 class 定义相似,类的访问标示符,取值范围如下
标志名称
标志值
含义
ACC_PUBLIC
0x0001
内部类是否为 public
ACC_PRIVATE
0x0002
内部类是否为 private
ACC_PROTECTED
0x0004
内部类是否为 protected
ACC_STATIC
0x0008
内部类是否为 static
ACC_FINAL
0x0010
内部类是否为 final
ACC_INTERFACE
0x0020
内部类是否为 接口
ACC_ABSTRACT
0x0400
内部类是否为 abstract
ACC_SYNTHETIC
0x1000
内部类是否并非由用户代码产生
ACC_ANNOTATION
0x2000
内部类是否为一个注解
ACC_ENUM
0x4000
内部类是否为一个枚举
Deprecated 及 Synthetic 属性 都是标志符类型的布尔属性,只有存在有和没有的区别,没有属性概念。Deprecated 对应 @deprecated 注解,表示不推荐使用。
Synthetic 标示字段或方法由编译器产生,JDK5之后同样的功能可以通过设置 ACC_SYNTHETIC 标志位达到。通过这种方式甚至可以越权访问或绕开语言限制功能。典型例子是枚举类中自动生成枚举元素数组和嵌套类的桥接方法(Bridge Method)。
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
attribute_length 必须为 0x00000000,因为诶呦任何属性需要设置。
StackMapTable 属性 JDK6 增加到 Class 文件规范,一个相当复杂的变长属性,位于 Code 属性表中,用来代替原来的类型检查验证器,提升性能。实现很复杂,Java SE7 新增 120 页篇幅讲解描述。
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
number_of_entries
1
stack_map_frame
stack_map_frame entries
number_of_entries
SE7 之后规定,版本号 >= 50.0 的 class 文件都必须带有 StackMapTable 属性。一个 Code 属性最多只能有一个 StackMapTable 不然抛错 ClassFormatError。
Signature 属性 在 JDK5 中和泛型一起加入的,记录泛型签名信息。Java 中的泛型是伪泛型。
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
signature_index
1
signature_index 指向常量池的一个 CONSTANT_Utf8_info 索引。
BootstrapMethods 属性 JDK7 时新增,JDK8 中通过 lambda 发扬光大。位于类文件属性表中,用于保存 invokeDynamic 指令引用的引导方法限定符。类文件常量池中出现过 CONSTANT_InvokeDynamic_info 类型的常量,那么属性表中必有 BootstrapMethods 属性,一个类文件中至多只能有一个 BootstrapMethods 属性。
BootstrapMethods 属性结构
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
num_bootstrap_methods
1
bootstrap_method
bootstrap_methods
num_bootstrap_methods
bootstrap_methods[]: 每个成员包含一个指向常量池 CONSTANT_MethodHandle 结构的索引,代表一个引导方法。
bootstrap_method 属性结构
type
name
count
u2
bootstrap_method_ref
1
u2
num_bootstrap_arguments
1
u2
bootstrap_arguments
num_bootstrap_arguments
bootstrap_method_ref:对常量池的一个有效索引,索引处必须是一个 CONSTNAT_MethodHandle_info 结构
num_bootstrap_arguments:arg 数量
bootstrap_arguments:每个成员必须是对常量池的有效引用,指向的结构必须是:CONSTANT_String_info,CONSTANT_Class_info, CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_MethodHandle_info 或 CONSTANT_MethodType_info 之一
MethodParameters 属性 JDK8 时加入,之前没有这个属性, jar 包反编译时缺少参数信息,不方便理解,影响传播。之前还有个替代方案,通过 ‘-g:var’ 存入 LocalVariableTable, 但是他时 Code 的字表,在接口方法这类没有具体实现的方法时不生效。
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u1
parameters_count
1
parameter
parameters
parameters_count
parameter 属性
type
name
count
u2
name_index
1
u2
access_flags
1
name_index 指向常量池 CONTANT_Utf8_info 的索引值,代表名称
access_flags 有三种 0x0001-ACC_FINAL, 0x1000-ACC_SYNTHETIC, 0x8000-ACC_MANDATED(原文件中隐式定义,典型用法 this)
模块化相关属性 TBD 怎是没用到就不记了,以后用到再看看
运行时注解相关属性 JDK5 时加入了注解相关信息到 Class 文件,他们是 RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations。JDK8 时新家了 RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations。这些属性功能和结构都很雷同。
RuntimeVisibleAnnotations 属性结构
type
name
count
u2
attribute_name_index
1
u4
attribute_length
1
u2
num_annotations
1
annotation
annotations
num_annotations
annotations 属性结构
type
name
count
u2
type_index
1
u2
num_element_value_pairs
1
element_value_pair
element_value_pairs
num_element_value_pairs
type_index 指向常量池 CONSTANT_Utf8_info 常量的索引, num_element_value_pairs 数组计数器,element_value_pair 为键值对
6.4 字节码指令简介 虚拟机指令 = 操作码(opcode) + 操作数(oprand)
操作码为一个字节长度,操作数为 0 至 n 个,虚拟机执行模型
1 2 3 4 5 6 do { 自动计算 PC 寄存器的值加 1; 根据 PC 寄存器指示的位置,从字节码流中取出操作码; if (字节码存在操作数) 从字节码流中取出操作数; 执行操作吗所定义的操作; } while (字节码流长度 > 0)
6.4.1 字节码与数据类型 大多数操作码都包含对应操作数类型信息,比如 iload。
i - int
l - long
s - short
b - byte
c - char
f - float
d - double
a - reference
boolean, byte, short, char 在编译时会被扩展成 int 类型再处理。
6.4.2 加载和存储指令 用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
将局部变量加载到操作栈:(i/l/f/d/a)load, (i/l/f/d/a)load_
将一个数值从操作数栈存储到局部变量表:(i/l/f/d/a)store, (i/l/f/d/a)store_
将一个常量加载到操作数栈:bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, icont_, lconst_, fconst_, dconst_
扩充局部变量表的访问索引指令: wide
iload_ 代表了 iload_1, iload_2, iload_3
6.4.3 运算指令 算术指令用于对 操作数栈 上的两个值进行某种特定运算,并把 结果 重新存入操作栈 顶。byte, short, char 和 Boolean 会转化为 int 计算
加法指令: (i, l, f, d)add
减法指令: (i, l, f, d)sub
乘法指令: (i, l, f, d)mul
除法指令: (i, l, f, d)div
求余指令: (i, l, f, d)rem
取反指令: (i, l, f, d)neg
位移指令: ishl, ishr, iushr, lshl, lshr, lushr
按位或指令: ior, lor
按位与指令: iand, land
按位异或指令: ixor, lxor
局部变量自增指令: iinc
比较指令: dcmpg, dcmpl, fcmpg, fcmpl, lcmp
6.4.4 类型转换指令 该指令可以将两种不同数值类型的数据互相转化,这些转化操作一般用于用户代码中的显示类型转化,或者前面提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
虚拟机直接支持宽化类型转化,及小范围向大范围转换
int 类型到 long, float, double
long 到 float, double
float 到 double
窄化转化指令: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f。
窄化转化可能发生上限溢出,下限溢出 或精度丢失,但是这些问题都不会抛出运行时异常。
6.4.5 对象创建与访问指令
创建类实例 new
创建数组 newarray, anewarray, multianewarray
访问类字段和实例字段的指令:getfield, putfield, getstatic, putstatic
把一个数组元素加载到操作数栈中的指令:baload, caload, saload, iaload, laload, faload, daload, aaload
将一个操作数栈的值存储到数组元素中:bastore, castore, sastore, iastore, fastore, dastore, aastore
取数组长度的指令:arraylength
检查类实例类型的指令:instanceof, checkcast
6.4.6 操作数栈管理指令
将操作数栈栈顶的一个或两个元素出栈:pop, pop2
复制栈顶的一个或两个数值并将复制或双份复制值重新压入栈顶:dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2
6.4.7 控制转移指令 可以让 Java 虚拟机有条件或五天见的从指定位置指令的吓一跳指令继续执行程序。
条件分支: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icompgt, if_icomple, if_icompge, if_acmpeq, if_acmpne
复合条件分支:tableswitch, lookupswitch
无条件分支:goto, goto_w, jsr, jsr_w, ret
6.4.8 方法调用和返回指令
invokevirtual: 调用对象的实例方法,根据对象的世纪类型进行分派,Java 中最常见的分派方式
invokeinterface: 调用接口方法,运行时搜索一个实现了该接口方法的对象,找出适合的方法进行调用
invokespecial: 调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
invokestatic: 调用静态方法
invokedynamic: 运行时动态解析出调用点限定符所应用的方法,并执行该方法。
返回指令:当返回值是 boolean, byte, char, short, int 时使用 ireturn, 其他还有 lreturn, freturn, dreturn 和 areturn。还有为 void 准备的 return。
6.4.9 异常处理指令 Java 中显示的排除异常操作由 athrow 指令实现,虚拟机中异常处理不是由字节码指令实现,而是通过 异常表
6.4.10 同步指令 虚拟机支持方法级别的同步和方法内部一段指令序列的同步,这两种同步结构都是用管程,也叫锁。方法级别的管程是隐示的无需通过字节码指令控制。他的实现在方法调用和返回之间。虚拟机可以重常量池方法表结构中的 ACC_SYNCHRONIZED 得知是否被声明为同步方法。如果执行时出现异常,同步方法所持有的锁会在异常抛到同步方法边界之外时自动释放。对应的指令为 monitorenter 和 monitorexit。
虚拟机必须保证每条 monitorenter 指令都有一条 monitorexit 指令与之对应。
6.5 公有设计,私有实现 Class 文件格式和字节码集是完全独立于操作系统和虚拟机实现的,任何一款虚拟机实现都必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的语义。虚拟机规范鼓励在满足约束的条件下修改和优化实现。虚拟机实现方式主要有两种:
将输入的 Java 虚拟机代码在加载或执行时翻译成另一种虚拟机代码
将输入的 Java 虚拟机代码在加载或执行时翻译成宿主机本地指令集,即 即时编译器代码生成技术
6.6 Class 文件结构的发展 相对与 Java 技术体系的变化,Class 文件结构可谓是相当的稳定了。。。