深入理解java虚拟机——类文件结构
Class 文件是一组以 8 位字节为基础单位的二进制流,其文件格式采用一种类似C语言「 结构体 」的结构存储数据,这种伪结构体只有两种数据类型:无符号数和表。
无符号数以 u1,u2,u4,u8 分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。
表是由多个无符号数或其他表构成的复合数据结构,习惯以 _info 结尾。整个 Class 文件本质上就是一张表。
无论无符号数还是表,当需要表示同一类型但数量不定的多数据时,常会采用一个前置容量计数器加若干连续数据项的形式。这一系列连续的某一类型数据称为某一类型的集合。
本文将以此 java 代码生成的 Class 文件为例。
public class TestClass {
private int m ;
public int inc(){
return m+1;
}
}
cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 154c 436c 6173 7346
696c 652f 5465 7374 436c 6173 733b 0100
0369 6e63 0100 0328 2949 0100 0a53 6f75
7263 6546 696c 6501 000e 5465 7374 436c
6173 732e 6a61 7661 0c00 0700 080c 0005
0006 0100 1343 6c61 7373 4669 6c65 2f54
6573 7443 6c61 7373 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7400 2100 0300
0400 0000 0100 0200 0500 0600 0000 0200
0100 0700 0800 0100 0900 0000 2f00 0100
0100 0000 052a b700 01b1 0000 0002 000a
0000 0006 0001 0000 0006 000b 0000 000c
0001 0000 0005 000c 000d 0000 0001 000e
000f 0001 0009 0000 0031 0002 0001 0000
0007 2ab4 0002 0460 ac00 0000 0200 0a00
0000 0600 0100 0000 0900 0b00 0000 0c00
0100 0000 0700 0c00 0d00 0000 0100 1000
0000 0200 11
魔数:
cafe babe
每个 Class 文件头 4 字节称为魔数(magic_number),用于确定这个文件是否为一个 Class 文件,具有固定的值:0xCAFEBABE(caffe baby~)
版本号:
cafe babe 0000 0034
接下来的 4 字节存储 Class 的版本号。
前 2 字节 0x0000 代表次版本号(minor_version),后 2 字节代表主版本号(major_version),0x0034 即十进制 52,代表 JDK 版本为 1.8。
常量池:
0016
-
常量池可以理解为 Class 文件的「 资源仓库 」。
由于常量池的常量数量不是固定的,因此在常量池的入口处,即主版本号后两个字符代表常量池容量计数值(constant_pool_count)。
常量池容量计数从 1 开始,第 0 号常量用于在特定情况下需要表达 “ 不引用任何一个常量池项目 ”。这里常量池容量为
0x0016,代表共有 21 个常量。常量池主要存放两种常量:
字面量及
符号引用。
字面量即字符串、声明为 final 的常量值等。
符号引用包括三类常量:
-
类和接口的全限定名
-
字段名称和描述符
-
方法名称和描述符
常量池中的每一个常量都是一个表,共有 14 种各不相同的表结构。每个表第一位均为 u1 类型的标志位,代表当前常量的类型:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码字符串 |
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 | String类型字面值 |
CONSTANT_Fieldref_info | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中声明方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中声明方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
常量项结构表
常量 | 项目 | 类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向字段或方法名称常量项目的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 |
0a00 0400 12
第 1 项常量标志位 0x0a ,即十进制 10,对应 CONSTANT_Methodref_info,代表方法的符号引用。
第 1 个 index —— 0x0004 为 CONSTANT_Utf8_info 类型常量,代表此方法所在类的全限定名,指向常量池的第 4 个常量。第 2 个 index —— 0x0012 为 CONSTANT_NameAndType_info 类型,指向第 18 个常量。
09 0003 0013
第 2 项常数标志位 0x09,代表 CONSTANT_Fieldref_info,即字段的符号引用。
0700 14
第 3 项常数标志位 0x07,代表 CONSTANT_Class_info 。 index 为类的全限定名,指向第 20 个常量。
07 0015
第 4 项常数标志位 0x07,index 指向第 21 个常量。
0100 016d
第 5 项常数标志位 0x01,代表 CONSTANT_Utf8_info 。length 为 0x0001,即 1 字节。 bytes 为长度 1 字节的字符串:0x6d,对应ASCII码的 ‘ m ’ 。
0100 0149
同理,第 6 项常数为 ‘ I ’。
0100 063c 696e 6974 3e
第 7 项——“ < init > ”
01 0003 2829 56
第 8 项——“ ()V ”
01 0004 436f 6465
第 9 项——“ Code ”
0100 0f4c 696e 654e 756d 6265 7254 6162 6c65
第 10 项——“ LineNumberTable ”
0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
第 11 项——“ LocalVariableTable ”
01 0004 7468 6973
第 12 项——“ this ”
0100 154c 436c 6173 7346 696c 652f 5465 7374 436c 6173 733b
第 13 项——“ LClassFile/TestClass; ”
0100 0369 6e63
第 14 项——“ inc ”
0100 0328 2949
第 15 项——“ ()I ”
0100 0a53 6f75 7263 6546 696c 65
第 16 项——“ SourceFile ”
01 000e 5465 7374 436c 6173 732e 6a61 7661
第 17 项——” TestClass.java “
0c00 0700 08
第 18 项标志位对应 CONSTANT_NameAndType_info。
第 1 个 index 指向第 7 个常数,第 2 个 index 指向第 8 个常数。即 “ < init >:()V ”
0c 0005 0006
第 19 项——“ m:I ”
0100 1343 6c61 7373 4669 6c65 2f54 6573 7443 6c61 7373
第 20 项——“ ClassFile/TestClass ”
0100 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74
第 21 项——“ java/lang/Object ”
当然,每次直接分析 Class 文件效率太低了~,java 提供了专门用于分析 Class 文件的工具 javap 。
访问标志:
常量池结束后,接下来的 2 个字节代表访问标志( access_flags )。用于识别一些类或接口的「访问信息 」。
包括 Class 是类还是接口,是否为 public,是否定义为 abstract 类型,是否声明为 final 等。
标志名 | 标志值 | 标志含义 | 针对的对象 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
00 21
在本例中 ACC_PUBLIC,ACC_SUPER 为真。
将标志为真的 flag 进行掩码实现:
0x0020|0x0001 = 0x0021
类索引、父类索引与接口索引集合
Class 文件中由这三项数据确定这个类的「 继承关系 」。类索引为这个类的全限定名,父类索引为这个类的父类,除 Object 外,所有类的父类索引不能为0。
类索引、父类索引均是 u2 类型的数据,接口索引集合为一组 u2 类型的集合。入口第一项 u2 类型数据为接口计数器。
00 0300 0400 00
0x0003、0x0004、0x0000 对应常量池的第 3 项与第 4 项,接口为空。
字段表集合
字段表(field_info)集合用于描述接口或者类中声明的「 变量 」,包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。
一个字段使用一个字段表描述。字段表集合的入口 2 个字符同样代表容量计数值。
字段表格式:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段修饰符放在 access_flags 中,与类中的 access_flags 十分相似:
标志名 | 标志值 | 标志含义 |
---|---|---|
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 |
接下来是两项索引值:name_index、descriptor_index,代表字段的简单名称和方法的描述符。
描述符 | 含义 |
---|---|
B | 基本数据类型byte |
C | 基本数据类型char |
D | 基本数据类型double |
F | 基本数据类型float |
I | 基本数据类型int |
J | 基本数据类型long |
S | 基本数据类型short |
Z | 基本数据类型boolean |
V | 基本数据类型void |
L | 对象类型 |
00 0100 0200 0500 0600 00
示例类中字段共 0x0001 个。access_flags 值为 0x0002,代表为 private。name_index为‘ m ’。descriptor_index 为‘ I ’。attributes_count 为 0 。即“privat int m;”
方法表集合
Class 文件对方法的描述与对字段的描述十分相似。方法表的结构与字段表结构完全相同~
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
标志名 | 标志值 | 标志含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYHCHRONRIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否为编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否为编译器自动产生 |
方法中的代码经编译器译为字节码指令后,存放在方法属性表集合的 Code 属性中。
00 0200 0100 0700 0800 0100 09
0x0002 代表集合中共有两个方法,分别为编译器添加的实例构造器 < init > 和 inc() 方法。
以第一个方法为例: ACC_PUBLIC 标志为真,名称索引为 0x0007。描述符索引为 0x0008。属性表计数器为 1。属性名称索引为 0x0009 —— Code,说明此属性为方法的字节码描述,方法中的 java 代码,经编译器编译为字节码文件后,就存放在 Code 属性中。
属性表集合
用于描述某些场景「 专有信息 」,在 Class 文件、字段表、方法表中均可携带自己的属性表。属性表中不要求严格顺序,但一个符合规则的属性表应满足如下结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
虚拟机预定义属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
这里以 Code 属性为例:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
00 0100 0100 0000 052a b700 01b1
0x0001 代表操作数栈深度max_stack为 1。
0x0001 代表局部变量表所需深度max_locals为 1。
0x0005 代表字节码区域所占空间,依次读取 5 个字节,并按照字节码指令表翻译字节码指令。
0x2a —— aload_0,将第 0 个 Slot 中的 reference 类型的本地变量推送至操作数栈顶
0xb7—— invokespecial,以栈顶 reference 数据指向的对象作为方法接收者,调用此对象的实例构造方法,private 方法,或者其他父类方法。此方法存在 u2 类型参数说明调用那个方法。0x000a 代表常量池 < init >。
0xb1 ——对应指令为 return 返回此方法。