JVM字节码和类加载

类文件结构
1 | public class HelloWorld { |
他的.class文件为:
1 | 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 |
根据 JVM 规范,类文件结构如下:
1 | ClassFile { |
魔数
0~3 字节,表示是否是 class 类型的文件。
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
cafebabe代表是Java类型
版本
4~7 字节,表示类的版本。
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
34 是十六进制,对应十进制 52,表示类的版本是 Java 8。
常量池
| 常量类型 (Constant Type) | 标志值 (Value) | 描述 (Description) |
|---|---|---|
| CONSTANT_Utf8 | 1 | UTF-8 编码的字符串(如类名、方法名) |
| CONSTANT_Integer | 3 | 整型字面量 |
| CONSTANT_Float | 4 | 浮点型字面量 |
| CONSTANT_Long | 5 | 长整型字面量(占 2 个常量池槽位) |
| CONSTANT_Double | 6 | 双精度浮点型字面量(占 2 个常量池槽位) |
| CONSTANT_Class | 7 | 类或接口的符号引用 |
| CONSTANT_String | 8 | 字符串类型字面量 |
| CONSTANT_Fieldref | 9 | 字段的符号引用 |
| CONSTANT_Methodref | 10 | 类中方法的符号引用 |
| CONSTANT_InterfaceMethodref | 11 | 接口中方法的符号引用 |
| CONSTANT_NameAndType | 12 | 字段或方法的名称和类型描述符 |
| CONSTANT_MethodHandle | 15 | 方法句柄(用于支持动态语言) |
| CONSTANT_MethodType | 16 | 方法类型(用于支持动态语言) |
| CONSTANT_InvokeDynamic | 18 | 动态方法调用点(Lambda 表达式核心) |
8~9 字节,表示常量池长度:
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
23 十进制对应 35,表示常量池有 #1 ~ #34 项,#0 项不计入,也没有值。
#1
#1 项 0a 对应十进制 10,根据上表查询得知,表示 CONSTANT_Methodref,即方法信息。00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获取这个方法的所属类和方法名:
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
00 06 对应第 #6 项,表示 Class 信息,然后它又引用了第 #28 项,指明具体的 Class 是 java/lang/Object。
00 15 对应第 #21 项,表示方法名、参数类型和返回值类型。它分别引用 #7 项表名方法名是
然后再一项一项查下去就行了
访问标识与继承信息
| 标志名称 (Flag Name) | 标志值 (Value) | 中文含义 (Interpretation) |
|---|---|---|
| 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 | 标识这是一个注解类型(Annotation)。 |
| ACC_ENUM | 0x4000 | 标识这是一个枚举类型(Enum)。 |
00 21 (由上表中的 0x0001 和 0x0020 相加获得)表示该 class 是一个公共的类:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00 05 表示根据常量池中的 #5 项找到 本类 的全限定名:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00 06 表示根据常量池中的 #6 找到 父类 的全限定名:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00 00 表示该类实现的接口数量,此处为 0:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
Field 信息
00 00 表示成员变量数量,此处为 0:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
| 标识字符 (FieldType) | 对应类型 (Type) | 中文解释 (Interpretation) |
|---|---|---|
| B | byte | 有符号字节型 |
| C | char | Unicode 字符(使用 UTF-16 编码) |
| D | double | 双精度浮点型 |
| F | float | 单精度浮点型 |
| I | int | 整型 |
| J | long | 长整型(因为 L 给了对象,所以用 J) |
L 类名 ; |
reference | 对象引用类型(以 L 开头,分号 ; 结尾) |
| S | short | 有符号短整型 |
| Z | boolean | 布尔型(true 或 false) |
| [ | reference | 数组维度(一个 [ 代表一维数组) |
Method 信息
00 02 表示方法数量,本类为 2(默认无参构造方法与 main 方法):
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
一个方法由访问修饰符、名称、参数描述、方法属性数量、方法属性组成。
eg:构造方法
1 | 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 |
附加属性
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即 SourceFile,表示字节码文件对应的 Java 源文件名称
00 00 00 02 表示此属性长度
00 14 表示引用了常量池 #20 项,即 HelloWorld.java
字节码指令
eg:
对应字节码指令:
b2 00 02 12 03 b6 00 04 b1
查询 JVM 规范得知,b2 对应 getstatic,用于加载静态变量
00 02 引用常量池中 #2 项,表示 getstatic 需要加载的静态变量信息,简单来说是 System.out
12 对应 ldc(load constant),用于加载参数
03 引用常量池中 #3 项,即字符串常量 Hello World
b6 对应 invokevirtual,预备调用成员方法
00 04 引用常量池中 #4 项,即 println 方法
b1 表示返回
javap
Oracle 提供了 javap 工具来反编译 class 文件:
1 | javap -v HelloWorld.class |
-v 参数表示输出 class 文件的详细信息。
图解方法执行流程
示例代码:
1 | public class Demo1 { |
对于 short 范围内的整数不会存入运行时常量池中,而是与字节码指令一起存放。short 的最大值是 32767,加一后大于最大值,因此 32768 会被存放到运行时常量池中。
方法字节码载入方法区

main 线程开始运行,分配栈帧内存

通过 javap 命令查看的字节码文件中存在:
1 | public static void main(java.lang.String[]); |
stack=2 表示操作数栈的深度是 2
locals=4 表示局部变量表有 4 个槽位
执行引擎开始执行字节码
bipush 10 表示将一个 byte 压入操作数栈(由于操作数栈的宽度占 4 个字节,压入内容不足 4 个字节时,会补齐 4 个字节),类似的指令还有:
sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
ldc 将一个 int 压入操作数栈
ldc2_w 将一个 long 压入操作数栈(因为 long 是 8 个字节,需要分两次压入)
小数字和字节码指令存放在一起,超过 short 范围的数字会存入常量池

istore_1 表示弹出操作数栈栈顶数据,存入局部变量表的 slot 1:

ldc #3 表示从运行时常量池中加载 #3 数据到操作数栈。

istore_2 表示弹出操作数栈栈顶数据,存入局部变量表的 slot 2:

接着从局部变量表里读取参与加法运算的两个数:
iload_1 表示从局部变量表 slot 1 里读取数据
iload_2 表示从局部变量表 slot 2 里读取数据

然后执行 iadd 弹出堆操作数栈里的两个整型数据进行相加:

再把运算结果压入操作数栈顶:

istore_3 再弹出操作数栈栈顶数据,存入局部变量表的 slot 3:

getstatic #4 从运行时常量池中获取成员变量 System.out 的引用,然后把该对象的 引用 添加进操作数栈中:

在调用 println 方法前,需要先加载所需的参数,使用 iload_3 从局部变量表 slot 3 里读取数据:

使用 invokevirtual #5 调用方法打印数据:
找到运行时常量池 #5 项
定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack 等)
传递参数,执行新栈帧中的字节码

目标方法执行完毕后,弹出栈帧。
清除 main 操作数栈的内容:

完成 main 方法调用后,弹出 main 栈帧,程序结束。
分析a++
1 | public class Demo1_2 { |
字节码为:
1 | public static void main(java.lang.String[]); |
分析:
iinc 指令是直接在局部变量 slot 上进行运算的
a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc
a++ 会被分解为两条字节码指令:
iload_1
iinc 1,1

++a 也会被分解为两条字节码指令,内容也与 a++ 一样,但顺序不同:
iinc 1,1
iload_1






条件判断指令
| 指令 | 助记符 | 含义 |
|---|---|---|
0x99 |
ifeq |
判断是否 $== 0$ |
0x9a |
ifne |
判断是否 $!= 0$ |
0x9b |
iflt |
判断是否 $< 0$ |
0x9c |
ifge |
判断是否 $>= 0$ |
0x9d |
ifgt |
判断是否 $> 0$ |
0x9e |
ifle |
判断是否 $<= 0$ |
0x9f |
if_icmpeq |
两个 int 是否 $==$ |
0xa0 |
if_icmpne |
两个 int 是否 $!=$ |
0xa1 |
if_icmplt |
两个 int 是否 $<$ |
0xa2 |
if_icmpge |
两个 int 是否 $>=$ |
0xa3 |
if_icmpgt |
两个 int 是否 $>$ |
0xa4 |
if_icmple |
两个 int 是否 $<=$ |
0xa5 |
if_acmpeq |
两个引用是否 $==$ |
0xa6 |
if_acmpne |
两个引用是否 $!=$ |