JVM3️⃣字节码技术①类文件结构、指令执行流程
1、类文件结构
类的字节码文件,是以 16 进制数表示的。
根据 JVM 规范,类文件结构包含以下信息:
-
魔数:文件类型,class 类型的魔数为 cafe babe
-
版本:类的版本,0x0034(十进制 52)表示 Java 8
-
常量池
-
访问标识、继承、接口信息
-
成员变量
-
方法
-
附加属性
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
javap
- 16 进制的 class 文件可读性很差,需要反编译。
- Oracle 提供 javap工具,用于反编译 class 文件。
- 指令:
javap -v HelloWorld.class
2、指令执行流程(❗)
2.1、图示流程
使用图形 + 字节码指令,演示字节码指令的详细执行过程。
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2.1.1、字节码文件
展示常量池、方法信息。
-
stack:操作数栈的容量。
-
locals:局部变量表的长度,每个位置称为 slot。
-
args_size:方法参数个数。
Constant pool: #1 = Methodref #7.#26 // java/lang/Object."<init>":()V #2 = Class #27 // java/lang/Short #3 = Integer 32768 #4 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream; #5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V #6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1 #7 = Class #33 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1; #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 args #18 = Utf8 [Ljava/lang/String; #19 = Utf8 a #20 = Utf8 I #21 = Utf8 b #22 = Utf8 c #23 = Utf8 MethodParameters #24 = Utf8 SourceFile #25 = Utf8 Demo3_1.java #26 = NameAndType #8:#9 // "<init>":()V #27 = Utf8 java/lang/Short #28 = Class #34 // java/lang/System #29 = NameAndType #35:#36 // out:Ljava/io/PrintStream; #30 = Class #37 // java/io/PrintStream #31 = NameAndType #38:#39 // println:(I)V #32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1 #33 = Utf8 java/lang/Object #34 = Utf8 java/lang/System #35 = Utf8 out #36 = Utf8 Ljava/io/PrintStream; #37 = Utf8 java/io/PrintStream #38 = Utf8 printl #39 = Utf8 (I)V { public cn.itcast.jvm.t3.bytecode.Demo3_1(); 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 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: ldc #3 // int 32768 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 17: return LineNumberTable: line 8: 0 line 9: 3 line 10: 6 line 11: 10 line 12: 17 LocalVariableTable: Start Length Slot Name Signature 0 18 0 args [Ljava/lang/String; 3 15 1 a I 6 12 2 b I 10 8 3 c I MethodParameters: Name Flags args }
2.1.2、类加载
常量池载入运行时常量池,方法字节码载入方法区。
2.1.3、main 线程
虚拟机栈与线程同时创建,线程私有。
- JVM 为每个线程分配一个虚拟机栈。
- 虚拟机栈 为每个方法分配一个栈帧。
- 每个栈帧都会分配局部变量表和操作数栈。
- 局部变量表
- 超过 short 范围的整数才会保存到常量池;
- 否则会保存在字节码指令中。
- 操作数栈
- 每个栈帧为 4 个字节;
- 入栈时不足用 0 补齐,超过时分 2 次入栈。
主线程开始运行,分配栈帧内存。
-
操作数栈:stack = 2,容量为 2。
-
局部变量表:locals = 4,长度为 4。
2.1.4、执行字节码
-
bipush 10
:将整数 10 入栈。 -
istore_1
:将栈顶数据(int,10),保存到局部变量表中下标为 1 的 slot。 -
ldc #3
:将常量池 #3 的整数入栈(该整数值在编译期计算得出)。 -
istore_2
:将栈顶数据(int,32768),保存到局部变量表中下标为 2 的 slot。 -
iload_1
:将局部变量表中 slot 1 的数据(int,10),加载到操作数栈的栈顶。 -
iload_2
:将局部变量表中 slot 2 的数据(int,32768),加载到操作数栈的栈顶。 -
iadd
:将操作数栈的两个整数相加并出栈,求和结果入栈 -
istore_3
:将栈顶数据(int,32778),保存到局部变量表中下标为 3 的 slot -
getstatic #4
:将常量池 #4 的静态变量入栈(对象在堆中的引用) -
iload_3
:将局部变量表中 slot 3 的数据(int,32778),加载到操作数栈的栈顶。 -
invokevirtual #5
-
加载常量池 #5(虚方法调用)
-
定位到对应方法
java/io/PrintStream.println:(I)V
-
生成新的栈帧,分配 locals 和 stack 等。
-
将参数传入新的栈帧,执行新栈帧中的字节码指令。
-
新栈帧中的方法执行完毕后,将栈帧从虚拟机栈中出栈。
-
清除 main 栈帧的操作数栈内容。
-
-
return
:main() 方法执行完毕,将 main 栈帧从虚拟机栈中出栈。 -
程序结束。
2.2、字节码指令(❗)
2.2.1、局部变量表
保存
将操作数栈的栈顶数据保存到局部变量表中相应 slot。
- astore:引用类型数据。
- 其它 load:基本类型数据。
- istore:int
- lstore:long
- fstore:float
- dstore:double
加载
将局部变量表中相应位置的数据,加载到操作数栈的栈顶。
- aload:引用类型数据。
- 其它 load:基本类型数据。
2.2.2、常量池
除了从局部变量表中加载数据,指令执行时加载的都是常量池的数据。
-
静态变量:getstatic
-
整数
- iconst:取 -1 到 5 时
- bipush:取 -128 到 127 时(byte 的范围)
- sipush:取 -32768 到 32767 时(short 的范围)
- ldc:取 -2147483648 到 2147483647 时(int 的范围)
- ldc2_w:取 -9223372036854775808到 9223372036854775807时(long 的范围)
2.2.3、算数指令
- 首字母 i、l、f、d,分别表示 int 、long、float、double
- byte、short、char 和 boolean 类型,都以 int 类型的指令代替。
int | long | float | double | |
---|---|---|---|---|
加 | iadd | ladd | fadd | dadd |
减 | isub | lsub | fsub | dsub |
乘 | imul | lmul | fmul | dmul |
除 | idiv | ldiv | fdiv | ddiv |
求余 | irem | lrem | frem | drem |
取反 | ineg | lneg | fneg | dneg |
位移 | ishl、ishr、iushr | lshl、lshr、lushr | ||
按位或 | ior | lor | ||
按位与 | iand | land | ||
按位异或 | ixor | lxor | ||
局部变量自增 | iinc | |||
比较指令 | lcmp | fcmpg、fcmpl | dcmp、dcmpl |
2.2.4、条件判断
比较栈顶数据:与 0 比较
指令 | 助记符 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 != 0 |
0x9b | iflt | 判断是否 < 0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
比较栈顶元素两个数据
- i:int 整数
- a:引用类型
指令 | 助记符 | 含义 |
---|---|---|
0x9f | if_icmpeq | == 0 |
0xa0 | if_icmpne | != 0 |
0xa1 | if_icmplt | < 0 |
0xa2 | if_icmpge | >= 0 |
0xa3 | if_icmpgt | > 0 |
0xa4 | if_icmple | <= 0 |
0xa5 | if_acmpeq | == |
0xa6 | if_acmpne | != |
2.2.5、方法调用
绑定方式 | 调用方法 | 备注 | |
---|---|---|---|
invokestatic | 静态 | 静态方法 | 通过类调用。 即使通过对象调用,也会先将对象引用弹出操作数栈,再通过类调用。 |
invokespecial | 静态 | 构造函数<init> 、私有方法、超类方法 |
调用构造函数后会将对象引用出栈,因此执行之前需要 dup 复制引用。 |
invokeinterface | 动态 | 接口方法 | |
invokevirtual | 动态 | 虚方法,除了以上的方法 | 通常用于多态 |
invokedynamic | 动态 | JDK 1.7 动态类型语言 |