class文件结构
class文件结构
class 文件介绍
什么是 class 文件?
能够被 JVM 识别,加载并执行的文件格式;很多语言可以生成 class 文件(Java、Scala、Python、Groovy、Kotlin)。
Java 之所以能够跨平台运行,是因为 Java 虚拟机可以载入和执行同一种平台无关的字节码。也就是说,实现语言平台无关性的基础是虚拟机和字节码存储格式,虚拟机并不关心 Class 的来源是什么语言,只要它符合 Class 文件应有的结构就可以在 Java 虚拟机中运行。
字节码文件由 十六进制
值组成,而 JVM 以两个十六进制值为一组,即以字节为单位进行读取。
如何生成一个 class 文件
- ide 自动生成
- javac 手动生成
class 文件的作用
记录一个类文件的所有信息,如名称,方法,变量等。
class 文件弊端
- 内存占用大,不适合移动端
- class 文件是堆栈的加载模式,加载速度慢
- 文件 IO 操作多,类查找慢
查看 class 二进制和字节码格式
源码:
1
2
3
4
5
6
package com.example.asm;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
查看 class 字节码
- javap,执行命令
javap -verbose HelloWorld
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
68
69
70
71
72
Classfile /Users/zengfansheng/Hacket/Workspace/king-assist/JavaTestCases/build/classes/java/main/com/example/asm/HelloWorld.class
Last modified 2021-8-26; size 566 bytes
MD5 checksum 8e2a168f70b6e4aeff39b67251df7750
Compiled from "HelloWorld.java"
public class com.example.asm.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/example/asm/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/example/asm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/example/asm/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.example.asm.HelloWorld();
descriptor: ()V
flags: 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 Lcom/example/asm/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
- AS 插件:
ASM Bytecode Viewer
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
// class version 52.0 (52)
// access flags 0x21
public class com/example/asm/HelloWorld {
// compiled from: HelloWorld.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/asm/HelloWorld; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 6 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello World!"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 7 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
- jclasslib bytecode viewer 查看字节码
查看 class 二进制工具
class 文件格式详解
class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
当遇到需要占用 8 位字节以上空间的数据项 时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。(高位在前指 “Big-Endian”,即指最高位字节在地址最低位,最低位字节在地址最高位的顺序来存储数据,而 X86 等处理器则是使用了相反的 “Little-Endian” 顺序来存储数据)
JVM 规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图:
class 文件格式数据类型
Class 文件格式采用了一种类似于 C 语言结构体的伪结构来存储数据,而这种伪结构中有且只有两种数据类型:无符号数和表。
示例源码,下面的都是基于该源码:
1
2
3
4
5
6
7
8
9
10
11
//Math.java
package com.example.asm.clazz;
public class Math {
private int a = 1;
private int b = 2;
public int add() {
return a + b;
}
}
1. 无符号数
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来 描述数字、索引引用、数量值或者按照UTF-8 码构成字符串值。
2. 表
表是 由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “_info
” 结尾。表用于 描述有层次关系的复合结构的数据,而整个 Class 文件其本质上就是一张表。
一个 class 文件包含以下数据项:
描述 | 类型 | 解释 |
---|---|---|
magic | u4 | 魔数,固定:0x CAFE BABE |
minor_version | u2 | java 次版本号 |
major_version | u2 | java 主版本号 |
constant_pool_count | u2 | 常量池大小 |
constant_pool[1-constant_pool_count-1] | struct cp_info(常量表) | 字符串池 |
access_flags | u2 | 访问标志 |
this_class | u2 | 类索引 |
super_class | u2 | 父类索引 |
interfaces_count | u2 | 接口计数器 |
interfaces | u2 | 接口索引集合 |
fields_count | u2 | 字段个数 |
fields | struct field_info(字段表) | 字段集合 |
methods_count | u2 | 方法计数器 |
methods | struct method_info(方法表) | 方法集合 |
attributes_count | u2 | 属性计数器 |
attributes | struct attribute_info(属性表) | 属性集合 |
- magic:魔数 4 个字节,唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件
- minor_version: 2 个字节长,表示当前 Class 文件的次版号
- major_version:2 个字节长,表示当前 Class 文件的主版本号。
- constant_pool_count:常量池数组元素个数。
- constant_pool:常量池,是一个存储了 cp_info 信息的数组
- access_flags:表示当前类的访问权限,例如:public、private。
- this_class 和 super_class:存储了指向常量池数组元素的索引,this_class 中索引指向的内容为当前类名,而 super_class 中索引则指向其父类类名
- interfaces_count 和 interfaces:同上,它们存储的也只是指向常量池数组元素的索引。其内容分别表示当前类实现了多少个接口和对应的接口类类名。
- fields_count 和 fields::表示成员变量的数量和其信息,信息由 field_info 结构体表示。
- methods_count 和 methods:表示成员函数的数量和它们的信息,信息由 method_info 结构体表示。
- attributes_count 和 attributes:表示当前类的属性信息,每一个属性都有一个与之对应的 attribute_info 结构。
1、magic 魔数
每个 class 文件的头4 个字节称为魔数(Magic Number
),类型为 u4,它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的 Class 文件。
很多文件存储标准中都使用魔数来进行身份识别, 譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。并且,Class 文件的魔数获得很有 “ 浪漫气息 “,值为:0xCAFEBABE(咖啡宝贝)。class 文件魔数的值为 0xCAFEBABE。如果一个文件不是以 0xCAFEBABE 开头,那它就肯定不是 Java class 文件。
2、minor_version、major_version class 文件的主次版本号
紧接着魔数的4 个字节存储的是 Class 文件的版本号:第 5 和第 6 是次版本号(Minior Version),第 7 个和第 8 个字节是主版本号 (Major Version)。Java 的版本号是人 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加 1,高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生变化。JDK1.1 能支持版本号为 45.045.65535 的 Class 文件,JDK1.2 则能支持 45.046.65535 的 Class 文件。JDK1.7 可生成的 Class 文件主版本号的最大值为 51.0。
需要注意的是,虚拟机会拒绝执行超过其版本号的 Class 文件。
3、constant_pool_count、constant_pool (常量池数量、常量池)
常量池可以理解为 Class 文件之中的资源仓库,其它的几种结构或多或少都会最终指向到这个资源仓库之中。
此外,常量池是 Class 文件结构中与其他项 关联最多 的数据类型,也是 占用 Class 文件空间最大的数据项之一,同时它还是 在 Class 文件中第一个出现的表类型数据项。
- constant_pool_count 常量池数量
- constant_pool 常量池,从 1 开始,0 做特殊用;存放了对这个类的信息描述,例如类名、字段名、方法名、常量值、字符串等
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值 (constant_pool_count
)。Constant pool 是从 1 开始,它将第 0 项的常量空出来了。而这个第 0 项常量它具备着特殊的使命,就是当其他数据项引用第 0 项常量的时候,就代表着这个数据项不需要任何常量引用的意思。但尽管 constant_pool
列表中没有索引值为 0 的入口,缺失的这一入口也被 constant_pool_count
计数在内(当 constant_pool
中有 14 项,constant_poo_count
的值为 15)。
class 文件结构中只有常量池的容量计数是从 1 开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从 0 开始的。
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
Constant pool:
#1 = Methodref #5.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#21 // com/example/asm/clazz/Math.a:I
#3 = Fieldref #4.#22 // com/example/asm/clazz/Math.b:I
#4 = Class #23 // com/example/asm/clazz/Math
#5 = Class #24 // java/lang/Object
#6 = Utf8 a
#7 = Utf8 I
#8 = Utf8 b
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/example/asm/clazz/Math;
#16 = Utf8 add
#17 = Utf8 ()I
#18 = Utf8 SourceFile
#19 = Utf8 Math.java
#20 = NameAndType #9:#10 // "<init>":()V
#21 = NameAndType #6:#7 // a:I
#22 = NameAndType #8:#7 // b:I
#23 = Utf8 com/example/asm/clazz/Math
#24 = Utf8 java/lang/Object
第 9 位代表的是 constant_pool_count 值为 001D,十进制就是 29,表示常量池有 29-1=28 个;第 11 位为 0A 代表的是常量 tag 值,十进制为 11,查询上表可知,代表的是 CONSTANT_Methodref
常量类型,下面的几位代表的是 class_index
和 name_and_type_index
。
如上所述,虚拟机加载 Class 文件的时候,就是这样从常量池中得到相对应的数值。
cp_info(常量表)
connstant_pool 中存储了一个一个的 cp_info 信息,并且每一个 cp_info 的第一个字节(即一个 u1 类型的标志位,tag,取值为 1 至 12,缺少标志为 2 的数据类型)标识了当前常量项的类型,其后才是具体的常量项内容
cp_info 主要存放字面量(Literal)和符号引用(Symbolic References)。
字面量(Literal)
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等
符号引用(Symbolic References)
而 符号引用 则属于编译原理方面的概念,包括了 三类常量,如下所示:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor))
- 方法的名称和描述符
在虚拟机加载 Class 文件的时候会进行动态链接,因为其字段、方法的符号引用不经过运行期转换的话就无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时进行解析,并翻译到具体的内存地址之中
常量项(tag 常量项对应的类型)
tag 常量项的类型,它主要包含以下 14 种类型:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |
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_MothodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
常量项数据结构
CONSTANT_String 和 CONSTANT_Utf8 的区别
CONSTANT_Utf8
真正存储了字符串的内容,其对应的数据结构中有一个字节数组,字符串便酝酿其中。CONSTANT_String
本身不包含字符串的内容,但其具有一个指向CONSTANT_Utf8
常量项的索引。
在所有常见的常量项之中,只要是需要表示字符串的地方其实际都会包含有一个指向 CONSTANT_Utf8_info 元素的索引。而一个字符串最大长度即 u2 所能代表的最大值为 65536,但是需要使用 2 个字节来保存 null 值,所以一个字符串的最大长度为 65534
常量项 Utf8
1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
- tag:值为 1,表示是 CONSTANT_Utf8_info 类型表
- length:length 表示 bytes 的长度,比如 length = 10,则表示接下来的数据是 10 个连续的 u1 类型数据。
- bytes:u1 类型数组,保存有真正的常量数据
常量项 Class、Filed、Method、Interface、String
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
// Class
CONSATNT_Class_info {
u1 tag;
u2 name_index;
}
// Field
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
// Method
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
// Interface
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
// String
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
CONSATNT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index
}
- name_index 指向常量池中索引为 name_index 的常量表。比如 name_index = 6,表明它指向常量池中第 6 个常量。
- class_index:指向当前方法、字段等的所属类的引用。
- name_and_type_index:指向当前方法、字段等的名字和类型的引用。
- descriptor_index:指向某字段或方法等的类型字符串的引用。
常量项 Integer、Long、Float、Double
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CONSATNT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
在每一个非基本类型的常量项之中,除了其 tag 之外,最终包含的内容都是字符串。正是因为这种互相引用的模式,才能有效地节省 Class 文件的空间。(ps:利用索引来减少空间占用是一种行之有效的方式)
案例分析:一个 class 二进制
如何查看一个 class 二进制?以一个案例来分析:
这是常量池第一个元素,分析其中的元素 (就是 Math 类的默认构造方法)
- tag,类型 u1,占用一个字节,为十六进制的 10(对应十进制 15),查看表格得知,这是一个
CONSTANT_Methodref
的结构 - class_index 声明方法的 class 的类型描述符
CONSTANT_Class_info
,可以看到为 5(对应常量池的索引 4)
- u2 name_and_type_index 指向名称和类型描述符 CONSTANT_NameAndType 的索引值, 索引为 20
- u2 name_index
- bytes 为
- u2 descriptor_index
- bytes 为 ()V
4、access_flags 访问标记 class 是否为抽象类、静态类
紧接常量池后的 2 个字节称为 access_flags
,它展示了文件中定义的类或接口的几段信息。
Class 的 access_flags 取值类型
标志名称 | 十六进制标志值 | 含义 |
---|---|---|
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 | 枚举类型 |
Field 的 access_flag 取值类型
标志名称 | 十六进制标志值 | 含义 |
---|---|---|
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,字段为枚举类型 |
Method 的 access_flag 取值
标志名称 | 十六进制标志值 | 含义 |
---|---|---|
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 | bridge,方法由编译器产生 |
ACC_VARARGS | 0x0080 | 该方法带有变长参数 |
ACC_NATIVE | 0x0100 | native |
ACC_ABSTRACT | 0x0400 | abstract |
ACC_STRICT | 0x0800 | strictfp |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成,由编译器自动生成 |
当 Method 的 access_flags 的取值为 ACC_SYNTHETIC 时,该 Method 通常被称之为合成函数。此外,当内部类访问外部类的私有成员时,在 Class 文件中也会生成一个 ACC_SYNTHETIC 修饰的函数。
5、this_class 当前类的名称
访问标志后面接下来的两个字节是类索引 this_class
,它是一个对常量池的索引。
在 this_class 位置的常量池入口必须为 CONSTANT_Class_info 表。该表由两个部分组成——tag 和 name_index。tag 部分是代表其的标志位,name_index 位置的常量池入口为一个包含了类或接口全限定名的 CONSTANT_Utf8_info 表。
指向:
其中 bytes 的值为 �com/example/asm/clazz/Math
6、super_class 父类的名称
在 class 文件中,紧接在 this_class 之后是 super_class
项,它是一个两个字节的常量池索引。
在 super_class 位置的常量池入口是一个指向该类超类全限定名的 CONSTANT_Class_info 入口。因为 Java 程序中所有对象的基类都是 java.lang.Object 类,除了 Object 类以外,常量池索引 super_class 对于所有的类均有效。对于 Object 类,super_class 的值为 0。对于接口,在常量池入口 super_class 位置的项为 java.lang.Object
7、interfaces_count 和 interfaces 该类的所有接口(只计算直接父接口)
紧接着 super_class 的是 interfaces_count
,此项的含义为:在文件中出该类直接实现或者由接口所扩展的父接口的数量。
在这个计数的后面,是名为 interfaces 的数组,它包含了对每个由该类或者接口直接实现的父接口的常量池索引。
每个父接口都使用一个常量池中的 CONSTANT_Class_info 入口来描述,该 CONSTANT_Class_info 入口指向接口的全限定名。这个数组只容纳那些直接出现在类声明的 implements 子句或者接口声明的 extends 子句中的父接口。超类按照在 implements 子句和 extends 子句中出现的顺序在这个数组中显现。
8、fields_count 和 fields 该类的所有字段
在 class 文件中,紧接在 interfaces 后面的是对在该类或者接口中所声明的字段的描述。
只有在文件中由类或者接口声明了的字段才能在 fields 列表中列出。在 fields 列表中,不列出从超类或者父接口继承而来的字段。另一方面,fields 列表可能会包含在对应的 Java 源文件中没有叙述的字段,这是因为 Java 编译器可以会在编译时向类或者接口添加字段。
- fields_count 的计数,它是类变量和实例变量的字段的数量总和。
- field_info 表的序列 (fields_count 指出了序列中有多少个 field_info 表)。
field_info
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但 不包括在方法内部声明的局部变量
。
field_info 数据结构:
1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
- access_flags 访问标志
- name 名字引用
- descriptor_index 描述信息引用
- attributes_count 属性数量
- attributes attribute_info 数组
9、method_count 和 methods 该类的所有方法
紧接着 field 后面的是对在该类或者接口中所声明的方法的描述。其结构与 fields 一样,不一样的是访问标志。
1
2
3
4
5
6
7
method_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
- access_flags 访问标志
- name 名字引用
- descriptor_index 描述信息引用
- attributes_count 属性数量
- attributes attribute_info 数组
类构造器为
< clinit >
方法,而实例构造器为< init >
方法;类构造器指的是 class 对象的构造器虚拟机调用;实例指的我们平时调用的构造器
10、attributes_count 和该类的所有属性(例如源文件名称,等等)
class 文件中最后的部分是属性,它给出了在该文件类或者接口所定义的属性的基本信息。属性部分由 attributes_count 开始,attributes_count 是指出现在后续 attributes 列表的 attribute_info 表的数量总和。每个 attribute_info 的第一项是指向常量池中 CONSTANT_Utf8_info 表的引引,该表给出了属性的名称。
属性有许多种。Java 虚拟机规范定义了几种属性,但任何人都可以创建他们自己的属性种类,并且把它们置于 class 文件中,Java 虚拟机实现必须忽略任何不能识别的属性。
Java 虚拟机预设的 9 项虚拟机应当能识别的属性如下表所示。
attribute_info attributes 数据结构:
1
2
3
4
5
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
- attribute_name_index 为 CONSTANT_Utf8 类型常量项的索引,表示属性的名称。
- attribute_length:属性的长度
- info:属性具体的内容
attribute_name_index
attribute_name_index 所指向的 Utf8 字符串即为属性的名称,而 属性的名称是被用来区分属性的。所有的属性名称如下所示(其中下面👇 加 * 的为重要属性):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1)、*ConstantValue:仅出现在 filed_info 中,描述常量成员域的值,通知虚拟机自动为静态变量赋值。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器方法中进行的;而对 于类变量,则有两种方式可以选择:在类构造器方法中或者使用 ConstantValue 属性。如果变量没有被 final 修饰,或者并非基本类型及字 符串,则将会选择在方法中进行初始化。
2)、*Code:仅出现 method_info 中,描述函数内容,即该函数内容编译后得到的虚拟机指令,try/catch 语句对应的异常处理表等等。
3)、*StackMapTable:在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流 分析的类型推导验证器。它省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶 段将一系列的验证类型(Verification Types)直接记录在 Class 文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在 JDK 1.6 中首次提供,并在 JDK 1.7 中强制代替原本基于类型推断的字节码验证器。StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frames),其中的类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。
4)、*Exceptions:当函数抛出异常或错误时,method_info 将会保存此属性。
5)、InnerClasses:用于记录内部类与宿主类之间的关联。
6)、EnclosingMethod
7)、Synthetic:标识方法或字段为编译器自动生成的。
8)、*Signature:JDK 1.5 中新增的属性,用于支持泛型情况下的方法签名,由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。
9)、*SourceFile:包含一个指向 Utf8 常量项的索引,即 Class 对应的源码文件名。
10)、SourceDebugExtension:用于存储额外的调试信息。
11)、*LineNumberTable:Java 源码的行号与字节码指令的对应关系。
12)、*LocalVariableTable:局部变量数组/本地变量表,用于保存变量名,变量定义所在行。
13)、*LocalVariableTypeTable:JDK 1.5 中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加。
14)、Deprecated
15)、RuntimeVisibleAnnotations
16)、RuntimeInvisibleAnnotations
17)、RuntimeVisibleParameterAnnotations
18)、RuntimeInvisibleParameterAnnotations
19)、AnnotationDefault
20)、BootstrapMethods:JDK 1.7中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符。切记,类文件的属性表中最多也只能有一个 BootstrapMethods 属性。
Code_attribute
要注意 并非所有的方法表都必须存在这个属性,例如接口或者抽象类中的方法就不存在 Code 属性。
Code_attribute 的数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Code_attribute 中的各个元素的含义如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
attribute_name_index、attribute_length:attribute_length 的值为整个 Code 属性减去 attribute_name_index 和 attribute_length 的长度。
max_stack:为当前方法执行时的最大栈深度,所以 JVM 在执行方法时,线程栈的栈帧(操作数栈,operand satck)大小是可以提前知道的。每一个函数执行的时候都会分配一个操作数栈和局部变量数组,而 Code_attribure 需要包含它们,以便 JVM 在执行函数前就可以分配相应的空间。
max_locals:**为当前方法分配的局部变量个数,包括调用方式时传递的参数。long 和 double 类型计数为 2,其他为 1。max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。局部变量表中的 Slot 可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量 所占的 Slot 可以被其他局部变量所使用,Javac 编译器会根据变量的作用域来分配 Slot 给各个 变量使用,然后计算出 max_locals 的大小**。
code_length:为方法编译后的字节码的长度。
code:用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个 u1 类型的单字节。一个 u1 数据类型的取值范围为 0x000xFF,对应十进制的 0255,也就是一共可以表达 256 条指令。
exception_table_length:表示 exception_table 的长度。
exception_table:每个成员为一个 ExceptionHandler,并且一个函数可以包含多个 try/catch 语句,一个 try/catch 语句对应 exception_table 数组中的一项。
start_pc、end_pc:为异常处理字节码在 code[] 的索引值。当程序计数器在 [start_pc, end_pc) 内时,表示异常会被该 ExceptionHandler 捕获。
handler_pc:表示 ExceptionHandler 的起点,为 code[] 的索引值。
catch_type:为 CONSTANT_Class 类型常量项的索引,表示处理的异常类型。如果该值为 0,则该 ExceptionHandler 会在所有异常抛出时会被执行,可以用来实现 finally 代码。当 catch_type 的值为 0 时,代表任意异常情况都需要转向到 handler_pc 处进行处理。此外,编译器使用异常表而不是简单的跳转命令来实现 Java 异常及 finally 处理机制。
attributes_count 和 attributes:表示该 exception_table 拥有的 attribute 数量与数据。
在 Code_attribute 携带的属性中,”LineNumberTable” 与 “LocalVariableTable” 对我们 Android 开发者来说比较重要
LineNumberTable 属性
LineNumberTable 属性 用于 Java 的调试,可指明某条指令对应于源码哪一行。LineNumberTable 属性的结构如下所示:
1
2
3
4
5
6
7
8
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
其中最重要的是 line_number_table
数组,该数组元素包含如下 两个成员变量:
- start_pc:为 code[] 数组元素的索引,用于指向 Code_attribute 中 code 数组某处指令。
- line_number:为 start_pc 对应源文件代码的行号。需要注意的是,多个 line_number_table 元素可以指向同一行代码,因为一行 Java 代码很可能被编译成多条指令。
LocalVariableTable 属性
LocalVariableTable 属性用于 描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中。
LocalVariableTable 的数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
其中最重要的元素是 local_variable_table 数组,其中的 start_pc 与 length 这两个参数 决定了一个局部变量在 code 数组中的有效范围。
每个非 static 函数都会自动创建一个叫做 this 的本地变量,代表当前是在哪个对象上调用此函数。并且,this 对象是位于局部变量数组第 1 个位置(即 Slot = 0),它的作用范围是贯穿整个函数的。
在 JDK 1.5 引入泛型之后,LocalVariableTable 属性增加了一个 “ 姐妹属性 “: LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录 的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对于非泛型类型来 说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。
信息描述规则
对于 JVM 来说,其采用了字符串的形式来描述数据类型、成员变量及成员函数这三类,我们需要了解下 JVM 中的信息描述规则。
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
数据类型
数据类型通常包含有 原始数据类型、引用类型(数组),它们的描述规则分别如下所示:
原始数据类型
标志符 | 含义 |
---|---|
B | 基本数据类型 byte |
C | 基本数据类型 char |
D | 基本数据类型 double |
F | 基本数据类型 float |
I | 基本数据类型 int |
J | 基本数据类型 long |
S | 基本数据类型 short |
Z | 基本数据类型 boolean |
V | 基本数据类型 void |
L | 对象类型,如 Ljava/lang/Object |
引用数据类型
1
L + 全路径类名(其中的 "." 替换为 "/",最后加分号)
例如 String =>
Ljava/lang/String;
数组(引用类型)
1
[该类型对应的描述名
例如 int 数组 => “[I”,String 数组 => “[Ljava/lang/Sting;”,二维 int 数组 => “[[I”。
成员变量
在 JVM 规范之中,成员变量即 Field Descriptor
的描述规则如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
FiledDescriptor成员变量描述:FieldType
# 1、仅包含 FieldType 一种信息
FiledType:BaseType | ObjectType | ArrayType
# 2、FiledType 的可选类型
BaseType:B | C | D | F | I | J | S | Z
ObjectType:L + 全路径ClassName;
ArrayType:[ComponentType:
# 3、与 FiledType 的可选类型一样
ComponentType:FiledType
在注释 1 处,FiledDescriptor 仅仅包含了 FieldType 一种信息;注释 2 处,可以看到,FiledType 的可选类型为 3 中:BaseType、ObjectType、ArrayType,对于每一个类型的规则描述,我们在 数据类型 这一小节已详细分析过了。而在注释 3 处,这里 ComponentType 是一种 JVM 规范中新定义的类型,不过它是 由 FiledType 构成,其可选类型也包含 BaseType、ObjectType、ArrayType 这三种。此外,对于字节码来讲,如果两个字段的描述符不一致, 那字段重名就是合法的。
成员函数描述规则
在 JVM 规范之中,成员函数即 Method Descriptor 的描述规则如下所示:
1
2
3
4
5
6
7
8
9
10
MethodDescriptor方法描述: ( ParameterDescriptor* ) ReturnDescriptor
# 1、括号内的是参数的数据类型描述,* 表示有 0 至多个 ParameterDescriptor,最后是返回值类型描述
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType | VoidDescriptor
VoidDescriptor:
// 2、void 的描述规则为 "V"
V
MethodDescriptor 由两个部分组成,括号内的是参数的数据类型描述,表示有 0 至多个 ParameterDescriptor,最后是返回值类型描述
案例 1:void hello(String str)
1
(Ljava/lang/String;)V
案例 2:public void add(int a, int b)
1
(II)V
案例 3:public String getContent(int type)
1
(I)Ljava/lang/Object
Reference
美团,介绍的很好,用图表示
- 谈谈 Java 虚拟机——Class 文件结构
http://www.cnblogs.com/xiaoruoen/archive/2011/11/30/2267309.html - The class File Format
http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.2 - Java Class 文件格式及其简单 Hack
http://www.stay-stupid.com/?p=401 - https://juejin.cn/post/6844904116603486222