一、Class 文件定义
class 文件
: 一组以 8 个字节 为基础单位的二进制流。- 各个数据项目严格 按照顺序 紧凑地排列在文件之中。中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在 这样可以使得 class 文件非常紧凑, 体积轻巧, 可以被 JVM 快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
class文件
: 中的信息是一项一项排列的, 每项数据都有它的 固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或 8 个字节, 数据项的不同长度分别用 u1, u2, u4, u8 表示, 分别表示一种数据项在 class 文件中占据一个字节, 两个字节, 4 个字节和 8 个字节。
类型 | 名称 | 说明 | 数量 |
---|---|---|---|
u4 | magic | 魔数:识别 Class 文件格式 | 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 |
二、Class 文件组成部分
TestClass.java
代码如下:
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.calvin.example;
public class TestClass {
private int m;
public TestClass() {
}
public int inc() {
return this.m + 1;
}
public static void main(String[] args) {
TestClass testClass = new TestClass();
int inc = testClass.inc();
System.out.println("TestClass.main => inc: " + inc);
}
}
- 查看
TestClass.class
二进制信息

- 通过
javap -v TestClass.class
命令,查看 jvm 字节码文件内容。
shell
Classfile /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-class/target/classes/com/calvin/example/TestClass.class
Last modified 2023-11-13; size 952 bytes
MD5 checksum 8a70cabe0c1568e5998b3f54c567a9ac
Compiled from "TestClass.java"
public class com.calvin.example.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#33 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#34 // com/calvin/example/TestClass.m:I
#3 = Class #35 // com/calvin/example/TestClass
#4 = Methodref #3.#33 // com/calvin/example/TestClass."<init>":()V
#5 = Methodref #3.#36 // com/calvin/example/TestClass.inc:()I
#6 = Fieldref #37.#38 // java/lang/System.out:Ljava/io/PrintStream;
#7 = Class #39 // java/lang/StringBuilder
#8 = Methodref #7.#33 // java/lang/StringBuilder."<init>":()V
#9 = String #40 // TestClass.main => inc:
#10 = Methodref #7.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #7.#42 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #7.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = Class #46 // java/lang/Object
#15 = Utf8 m
#16 = Utf8 I
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lcom/calvin/example/TestClass;
#24 = Utf8 inc
#25 = Utf8 ()I
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 testClass
#31 = Utf8 SourceFile
#32 = Utf8 TestClass.java
#33 = NameAndType #17:#18 // "<init>":()V
#34 = NameAndType #15:#16 // m:I
#35 = Utf8 com/calvin/example/TestClass
#36 = NameAndType #24:#25 // inc:()I
#37 = Class #47 // java/lang/System
#38 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#39 = Utf8 java/lang/StringBuilder
#40 = Utf8 TestClass.main => inc:
#41 = NameAndType #50:#51 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = NameAndType #50:#52 // append:(I)Ljava/lang/StringBuilder;
#43 = NameAndType #53:#54 // toString:()Ljava/lang/String;
#44 = Class #55 // java/io/PrintStream
#45 = NameAndType #56:#57 // println:(Ljava/lang/String;)V
#46 = Utf8 java/lang/Object
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 append
#51 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#52 = Utf8 (I)Ljava/lang/StringBuilder;
#53 = Utf8 toString
#54 = Utf8 ()Ljava/lang/String;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 println
#57 = Utf8 (Ljava/lang/String;)V
{
public com.calvin.example.TestClass();
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 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/calvin/example/TestClass;
public int inc();
descriptor: ()I
flags: 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 23: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/calvin/example/TestClass;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #3 // class com/calvin/example/TestClass
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #5 // Method inc:()I
12: istore_2
13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #7 // class java/lang/StringBuilder
19: dup
20: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
23: ldc #9 // String TestClass.main => inc:
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_2
29: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 13
line 30: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 args [Ljava/lang/String;
8 31 1 testClass Lcom/calvin/example/TestClass;
13 26 2 inc I
}
1.magic 魔数
二进制文件 第一行: CAFE BABE
- 魔数占用四个字节,判断能被 JVM 识别的.class 文件。
- 在 class 文件开头的
四个字节
, 存放着 class 文件的魔数。 - 这个魔数是 class 文件的标志。 它是一个固定的值:
0XCAFEBABE
。 - 也就是说他是判断一个文件是不是 class 格式的文件的标准, 如果开头四个字节不是 0XCAFEBABE, 那么就说明它不是 class 文件, 不能被 JVM 识别。
2.minor_version 次版本号 和 major_version 主版本号
二进制文件 第一行: 0000 0034
- 0000 代表次版本号, 0034 主版本号
- 对应字节码内容如下:
shell
public class com.calvin.example.TestClass
minor version: 0
major version: 52
- 高版本的 JDK 能够向下兼容低版本的 Class 文件,虚拟机会拒绝执行超过其版本号的 Class 文件。
- 根据以上信息可以获得主版本号信息 0x0034 的(3 * 16 + 4)十进制 52,52 对应的 JDK 版本是 JDK8,可以向下兼容 45-51 的 JDK 版本。JDK 版本是从 45 开始的,JDK1.0-1.1 使用了 45.0-45.3 的版本号。
3.constant_pool 常量池
二进制文件 第一行: 003A
- 常量池的容量为,A 在十进制为 10,所以计算 3*16 + 10 = 58 个。第一个 null 常用了一个常量,索引范围在 1-57。
- 常量池入口,常量池可以理解为 Class 文件之中的资源仓库。
- 因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型 的数据来表示 常量池的容量。
「constant_pool_count」
: 和计算机科学中计数的方法不一样,这个 容量是从 1 开始而不是从 0 开始计数。之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可以把索引值置为 0 来表示。- 常量池中主要存放两大类常量:
字面量和符号引用。
字面量
: 比较接近 Java 语言层面的常量概念,如 字符串、声明为 final 的常量值 等。符号引用
: 属于编译原理方面的概念,包括了以下三类常量。- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池—常量类型和结构
二进制文件 第一行中第一个常量: 0A 000E 0021
- 0A 为 10 => 标识位为 10,tag 为 u1 => 占用一个字节,属于 CONSTANT_Methodref_info 类中方法的符号引用。
- 000E 为 #14 => index 为 u2,占用两个字节, 指向常量池中的常量池中的位置 class_index 的索引项。
- 0021 为 #33 => index 为 u2, 占用两个字节,指向名称及类型描述符 nameAndType 的索引项。
对应字节码内容如下:
shell
#1 = Methodref #14.#33 // java/lang/Object."<init>":()V
二进制文件 第二行中第二个常量: 09 0003 0022
- 09 为 9 => 标识位为 9,tag 为 u1 => 占用一个字节,属于 CONSTANT_Fieldref_info 类中字段符号引用。
- 0003 为 #3 => index 为 u2,占用两个字节, 指向常量池中的常量池中的位置 class_index 的索引项。
- 0022 为 #34 => index 为 u2, 占用两个字节,指向名称及类型描述符 nameAndType 的索引项。
对应字节码内容如下:
shell
#2 = Fieldref #3.#34 // com/calvin/example/TestClass.m:I
二进制文件 第二行中第三个常量: 07 0023
- 07 为 7 => 标识位为 7,tag 为 u1 => 占用一个字节,属于 CONSTANT_Class_info 类或接口的符号引用。
- 0023 为 #35 => index 为 u2,占用两个字节, 指向全限定名常量项的索引
对应字节码内容如下:
shell
#3 = Class #35 // com/calvin/example/TestClass
二进制文件 第二行中第十九个常量: 01 0004 436F 6465
- 01 为 1 => 标识位为 1,tag 为 u1 => 占用一个字节,属于 CONSTANT_utf8_info utf-8 编码字符串。
- 0004 => 字符串长度为 4。
- 436F 6465 => 字节为
code
字符串。
对应字节码内容如下:
shell
#19 = Utf8 Code
- 常量池中每一项常量都是一个表,JDK1.7 之后共有 14 种不同的表结构数据。如下表格所示:
u1
:一个字节。u2
:两个字节。标志位1
: 表示字符串。length
: 表示字符串的字节长度。

4.access_flags 访问标志
二进制文件 第四十三行: 0021 00
- access_flag 的值为:0x0001|0x0020 = 0x0021
- 没有使用到的标志位要求一律为 0。
对应字节码内容如下:
shell
flags: ACC_PUBLIC
- 标志用于识别一些类或者接口层次的访问信息。
- 包括这个 Class 是类还是接口,是否定义为 public 类型, 是否定义为 abstract 类型,如果是类的话,是否被申明为 final 等。
- 具体的标志位以及标志的含义见下表(access_flags 中一共有 16 个标志位可以使用,当前只定义了其中的 9 个,没有使用到的标志位要求一律为 0。):
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
4.this_class 类索引
二进制文件 第四十四行: 0030
- 占用 2 个字节,使用 this_class 类描述常量
对应字节码内容如下:
shell
0: new #3 // class com/calvin/example/TestClass
- 一个 u2 类型的数据.
- 类索引用于确定这个类的全限定名.
- 指向 CONSTANT_Class_info 的类描述常量,通过 CONSTANT_Class_info 的类型常量中的索引可以找到,CONSTANT_Utf8_info 类型的常量中的全限定名字符串。从而获取到该类的全限定名
5.super_class 父类索引
二进制文件 第四十四行: 000E
- 占用 2 个字节,使用 super_class 类描述常量
- 指向的是第 14 个
对应字节码内容如下:
shell
#14 = Class #46 // java/lang/Object
- 一个 u2 类型的数据
- 父类索引用于确定这个类的父类的全限定名
- 指向 CONSTANT_Class_info的类描述常量,通过 CONSTANT_Class_info 的类型常量中的索引可以找到,CONSTANT_Utf8_info 类型的常量中的全限定名字符串。从而获取到该类的全限定名
6.interfaces 接口索引集合
二进制文件 第四十四行: 0000
- 父类索引之后为接口索引(interfaces) = 0x0000,因为没有实现任何接口,因此为全 0.
- 一组 u2 类型的数据集合
- 接口索引集合用于描述这个类实现了哪些接口
- Class 文件中由这三项数据(this_class、super_class、interfaces)来确定这个类的继承关系。
- 类索引、父类索引、接口索引都排在访问标志之后。由于所有的类都是 java.lang.Object 类的子类,因此除了 Object 类之外所有类的父类索引都不为 0。
7.fields 字段表集合
二进制文件 第四十四行: 0001 0002 000F 0010 0000
- 0001: 表示 fields_count 字段个数 1 个。
- 0002: 表示 access_flag 访问标识符 0x0002 => ACC_PRIVATE
- 000F: 表示 name_index 查看常量池表 #15 常量是 CONSTANT_Utf8_info,其值为 m。
- 0010: 表示 descriptor_index 该常量值索引 #16 常量是 CONSTANT_Utf8_info,其值为 I。
- 0000: 表示 attributes_count 属性表计数器为 0, 也就是没有需要额外描述的信息。
对应字节码内容如下:
shell
#15 = Utf8 m
#16 = Utf8 I
- 用于描述接口或者类中声明的变量。
- 字段(field) 包括
类变量
和实例变量
,但不包括方法内部声明的局部变量
。 - 字段表的结构,如下:
- 字段修饰符字段修饰符 放在 access_flags 中可设置的标志符有 ,它与类中的 access_flag 非常相似,都是一个 u2 的数据类型如下:
字段表结构表格
类型 | 名称 | 数量 |
---|---|---|
u2 | fields_count | 1 |
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 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_VOLATILE | 0x0040 | 字段是否为 volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为 transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0x4000 | 字段是否为 enum |
8.methods 方法表集合
二进制文件 第四十五行: 0003 0001 0011 0012 0001
- 0003: 表示 methods_count 方法个数 3 个(构造方法、inc 方法、main 方法)。
- 0001: 表示 access_flag 访问标识符 ACC_PUBLIC。
- 0011: 表示 name_index 查看常量池表 #17 名为 init 的方法。
- 0012: 表示 descriptor_index 该常量值索引 #18 代表 "V"的常量。
- 0001: 表示 attributes_count 方法的属性表集合有 1 项属性。
- 0013: 表示 attributes_name_index 属性名称的索引值为 0x0013,对应的常量为"Code",说明此属性是方法的字节码描述。
对应字节码内容如下:
shell
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
- Class 文件中对方法的描述和对字段的描述是完全一致的,方法表中的结构和字段表的结构一样。
- 因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT。
- 与之相对的 synchronizes、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志。
- 对于方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表中一个名为 Code 的属性里面。
方法表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 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_SYNCHRONIZED | 0x0020 | 方法是否为 synchronized |
ACC_BRIDGE | 0x0040 | 方法是不是有编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为 native |
ACC_ABSTRACT | 0x0400 | 方法是否为 abstract |
ACC_STRICT | 0x0800 | 方法是否为 strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动生成 |
9.attributes 属性表集合
- 在 Class 文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用于描述某些场景专有的信息。
- 属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。
- Java 虚拟机在运行时会略掉它不认识的属性。