[TOC]
Java中的各种操作和运算,都是基于方法进行的。 即便是静态变量和实例变量的初始化声明赋值(比如: public static int count = 1;
和 private int age = 18;
), 也会被归集到相应的初始化方法中。
Java虚拟机规范, 定义了class文件中使用的各种字节码, 其中方法使用的部分称为操作码, 也就是Java虚拟机指令集。 英文文档为: Chapter 6. The Java Virtual Machine Instruction Set
另外,官方单独整理了一份操作码助记符, 对应的链接为: Java Virtual Machine Specification: Chapter 7. Opcode Mnemonics by Opcode。 本文也按照这份操作码助记符的顺序进行介绍。
上一篇文章的最新版本请访问: 2020年文章: 41.深入JVM - 实例详解invoke相关操作码
本文的最新版本请访问: 2020年文章: 42.深入JVM - 案例讲解方法体字节码
本文基于Java SE 8 Edition的JDK进行讲解。
写一个简单的类, 其中包含main方法:
package com.cncounter.opcode;
/**
* 演示方法体字节码
*/
public class DemoMethodOpcode {
public static void main(String[] args) {
}
}
代码很简单, 编写完成后, 我们通过以下命令进行编译和反编译:
# 编译
javac -g DemoMethodOpcode.java
# 反编译
javap -v DemoMethodOpcode.class
# 想要运行main方法则需要注意包名; 比如:
mkdir -p com/cncounter/opcode/
cp DemoMethodOpcode.class com/cncounter/opcode/
java com.cncounter.opcode.DemoMethodOpcode
反编译工具 javap 输出的字节码信息如下:
Classfile /Users/renfufei/src/com/cncounter/opcode/DemoMethodOpcode.class
Last modified 2021-1-10; size 433 bytes
MD5 checksum 222c8d4911e85ed9e5d7e0b46dc9af29
Compiled from "DemoMethodOpcode.java"
public class com.cncounter.opcode.DemoMethodOpcode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // com/cncounter/opcode/DemoMethodOpcode
#3 = Class #19 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/cncounter/opcode/DemoMethodOpcode;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 SourceFile
#16 = Utf8 DemoMethodOpcode.java
#17 = NameAndType #4:#5 // "<init>":()V
#18 = Utf8 com/cncounter/opcode/DemoMethodOpcode
#19 = Utf8 java/lang/Object
{
public com.cncounter.opcode.DemoMethodOpcode();
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 Lcom/cncounter/opcode/DemoMethodOpcode;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
SourceFile: "DemoMethodOpcode.java"
下面分别对各个部分进行解读。
Classfile /Users/renfufei/src/com/cncounter/opcode/DemoMethodOpcode.class
Last modified 2021-1-10; size 433 bytes
MD5 checksum 222c8d4911e85ed9e5d7e0b46dc9af29
Compiled from "DemoMethodOpcode.java"
这里展示的信息包括:
- class文件的路径
- 修改时间, 文件大小(
433 bytes
)。 - MD5校验和
- 源文件信息(
"DemoMethodOpcode.java"
)
public class com.cncounter.opcode.DemoMethodOpcode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
从中可以解读出的信息包括:
- class的完全限定名信息:
com.cncounter.opcode.DemoMethodOpcode
- class文件的小版本号:
minor version: 0
- class文件的大版本号:
major version: 52
; 根据规则,52-45(+1.0) = 8
, 所以class格式对应的JDK版本为8.0
; - class的可见性标识:
ACC_PUBLIC
表示这是一个 public 类;ACC_SUPER
则是为了兼容JDK1.0而生成的, 可以忽略。
Constant pool:
#1 = Methodref #3.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // com/cncounter/opcode/DemoMethodOpcode
#3 = Class #19 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/cncounter/opcode/DemoMethodOpcode;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 SourceFile
#16 = Utf8 DemoMethodOpcode.java
#17 = NameAndType #4:#5 // "<init>":()V
#18 = Utf8 com/cncounter/opcode/DemoMethodOpcode
#19 = Utf8 java/lang/Object
简单解读一下:
- 最左边的
#1
,#2
等数字, 表示这个class文件的静态常量池中的item(条目)编号。 - item 编号后面的等号(
=
), 是反编译器为了方便展示,统一放置的。 #1 = Methodref #3.#17
, 1号item, 表示这个item是一个方法引用, 类引用参考#3
号常量, 方法名引用了#17
号常量。#3 = Class #19
, 3号item, 表示这个item是一个类引用, 类名引用了#19
号常量。#4 = Utf8 <init>
, 4号item, 表示这是一个UTF8字符串, 后面的<init>
就是常量item的值。#17 = NameAndType #4:#5
, 17号item, 表示一个方法的名称以及参数返回值信息;#4:#5
表示: 方法名引用#4
号item, 参数和返回值引用#5
号item。 当然, 后面的注释信息也说明了这一点。- 反编译器在有些条目后面展示了注释信息, 比如
// java/lang/Object
这种, 这样展示是为了方便理解。
详细的常量池信息解读, 可以参考Java SE 8 Edition的: JVM规范: 4.4. The Constant Pool
public com.cncounter.opcode.DemoMethodOpcode();
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 Lcom/cncounter/opcode/DemoMethodOpcode;
简单解读一下构造函数:
descriptor: ()V
: 方法描述符信息, 括号里面什么都没有, 表示不需要接收外部参数; 括号后面的V
表示没有返回值(类似于void)。flags: ACC_PUBLIC
: 访问标志, 表示这是一个public方法, 很明显, 编译器自动生成的默认构造方法就是:无参public构造方法
。stack=1, locals=1, args_size=1
: 首先说 stack=1,表示操作数栈的最大深度是1; 然后说 locals=1, 表示局部变量表的槽位数是1; 最后说 args_size=1, 表示参数个数=1; 为什么这几个值都是1呢? 本质上, 构造函数是一种特殊的实例方法, 在里面可以引用this
; 在执行实例方法时, 要先确定通过哪个对象来调用这个方法, 使用过反射机制来调用实例方法的同学应该会比较容易理解; JVM在调用之前需要先把this压进操作数栈, 然后拷贝/重用到构造函数的局部变量槽位中; 所以这里编译器自动生成的无参构造函数, 这几个属性的值都等于1.0: aload_0
这条指令, 前面的0
表示字节码的位置索引; 在执行跳转指令的时候, 其操作数引用的就是这种索引值(也可以叫指令偏移量), 在这里aload_0
指令的作用, 就是将局部变量表中0号槽位的变量值(this
)加载到操作数栈(压入), 供后续的其他指令使用。1: invokespecial #1
是位置索引偏移量=1的指令, 这个指令的助记符是invokespecial
, 在字节码文件中需要附带2个字节的操作数, 也就是后面跟着的#1
占了2个字节, 表示引用常量池中的1号item。 这条指令包括了操作码和操作数, 表示的意思是: 使用前一条指令压入操作数栈的对象, 调用特殊方法, 也就是Object类的初始化块<init>
方法。 最后,//
后面是注释信息, 是反编译器展示给我们人工阅读查看的。 这个指令, 在字节码文件中带2个字节的操作数(这个长度支持最大65536个)。4: return
表示方法结束并返回; 为什么指令前面的索引值是4呢? 参考前一条指令的说明, 索引位置2和索引位置3, 被invokespecial
指令的操作数占用了(可以简单推测, 一个方法中最多支持65536个常量, 如果超过了则会编译报错。 如果感兴趣的话, 可以写一个通过循环来生成java文件的程序, 往某个方法里面灌N多条语句, 参考文章: class文件中常量池条目数量与方法指令数限制)。LineNumberTable
表示与源代码对应的行号映射信息,line 6: 0
表示字节码的索引位置0处, 对应源码文件的第6行, 抛异常堆栈时挺有用, 当然,这个信息是可以被编译器擦除的, 如果编译器不生成那就没有了, 我们编译时指定了javac -g
参数则是强制生成调试信息。LocalVariableTable
则是局部变量表;- 可以看到0号槽位(Slot)存的是this值, 作用域范围则是(Start=0; Length=5;)
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
简单解读一下:
descriptor: ([Ljava/lang/String;)V
: 方法描述符信息, 括号里面是参数类型,[
打头代表数组,[L
表示对象类型数组; 括号后面的V
表示没有返回值(类似于void)。flags: ACC_PUBLIC, ACC_STATIC
访问标志, 表示这是一个 public 的 static 方法。stack=0, locals=1, args_size=1
: 表示操作数栈的最大深度=0, 因为是空方法, 里面没有什么压栈操作; 局部变量表槽位数=1, 一个引用变量只占用1个槽位, 特殊的是long和double占2个操作,这个后面会介绍; 接收的参数个数=1, 和前面的构造函数对比来看, static 方法不能使用this, 所以定义了几个入参就是几个;0: return
前面的0表示字节码的位置索引, return表示方法结束并返回; 因为这是一个空方法, 什么也没有。LineNumberTable
表示与源代码对应的行号映射信息,line 8: 0
是说此方法字节码的0索引对应第8行源码。LocalVariableTable
则是局部变量表;- 可以看到0号槽位(Slot)存的是
args
, 作用域范围是(Start=0; Length=1;), 对应方法体code的索引位置。
在继续之前, 先简单介绍字节码文件中, 方法局部变量表的规则:
- 如果是实例方法, 则局部变量表中第0号槽位中保存的是 this 指针。
- 然后排列的是方法的入参。 前面的this, 以及入参都是方法执行前就已经设置好的。
- 接下来就是按照局部变量定义的顺序, 依次分配槽位。
- 注意 long 和 double 会占据2个槽位, 那么可以算出每个槽位是32bit,也就是4字节, 这是历史债了。
- 可能存在匿名的局部变量以及槽位。
- 可能存在变量槽位重用的情况,依据局部变量的作用域范围而定, 这都是编译器干的事。
- 具体的汇总和映射信息, 在 class 文件中每个方法的局部变量表中进行描述。
局部变量表, LocalVariableTable
, 有时候也称为本地变量表,都是一回事,重点是理解其含义。
下面依次进行讲解,并通过实际的例子来加深理解。
常量相关的操作符, 大部分都很简单。 表示直接从字节码中取值, 或者从本类的运行时常量池中取值, 然后压入操作数栈的栈顶。
十进制 | 十六进制 | 助记符 | 附带操作数(字节) | 出栈 | 入栈 | 说明 |
---|---|---|---|---|---|---|
00 | (0x00) | nop | 0 | 0 | 0 | 没有操作, 可以看到编码是00 |
01 | (0x01) | aconst_null | 0 | 0 | 1 | 将常量 null 压入操作数栈的栈顶 |
02 | (0x02) | iconst_m1 | 0 | 0 | 0 | 将int常量值 -1 压入操作数栈 |
03 | (0x03) | iconst_0 | 0 | 0 | 0 | 将int常量值 0 压入操作数栈 |
04 | (0x04) | iconst_1 | 0 | 0 | 0 | 将int常量值 1 压入操作数栈 |
05 | (0x05) | iconst_2 | 0 | 0 | 0 | 将int常量值 2 压入操作数栈 |
06 | (0x06) | iconst_3 | 0 | 0 | 0 | 将int常量值 3 压入操作数栈 |
07 | (0x07) | iconst_4 | 0 | 0 | 0 | 将int常量值 4 压入操作数栈 |
08 | (0x08) | iconst_5 | 0 | 0 | 0 | 将int常量值 5 压入操作数栈 |
09 | (0x09) | lconst_0 | 0 | 0 | 0 | 将long常量值 0 压入操作数栈 |
10 | (0x0a) | lconst_1 | 0 | 0 | 0 | 将long常量值 1 压入操作数栈 |
11 | (0x0b) | fconst_0 | 0 | 0 | 0 | 将float常量值 0 压入操作数栈 |
12 | (0x0c) | fconst_1 | 0 | 0 | 0 | 将float常量值 1 压入操作数栈 |
13 | (0x0d) | fconst_2 | 0 | 0 | 0 | 将float常量值 2 压入操作数栈 |
14 | (0x0e) | dconst_0 | 0 | 0 | 0 | 将double常量值 0 压入操作数栈 |
15 | (0x0f) | dconst_1 | 0 | 0 | 0 | 将double常量值 1 压入操作数栈 |
16 | (0x10) | bipush | 0 | 0 | 0 | 将byte 常量值压入操作数栈, 后面带的操作数是1个字节 |
17 | (0x11) | sipush | 0 | 0 | 0 | 将short常量值压入操作数栈, 后面带的操作数占2个字节 |
18 | (0x12) | ldc | 0 | 0 | 0 | 将运行时常量池中的item压入操作数栈,load constant,后面带的操作数是1字节的常量池index |
19 | (0x13) | ldc_w | 0 | 0 | 0 | 将运行时常量池中的item压入操作数栈, 后面带的操作数是2字节的wide index |
20 | (0x14) | ldc2_w | 0 | 0 | 0 | 将运行时常量池中的long或者double值压入操作数栈, 后面带的操作数是2字节的index |
这一块很简单,也很容易记忆。
下面我们用代码来简单演示, 以加深印象。
package com.cncounter.opcode;
/**
* 演示常量相关的操作码
*/
public class DemoConstantsOpcode {
public static void testConstOpcode() {
int m1 = -1; // iconst_m1; istore_0;
int i0 = 0; // iconst_0; istore_1;
int i1 = 1; // iconst_1; istore_2;
int i2 = 2; // iconst_2; istore_3;
int i3 = 3; // iconst_3; istore 4;
int i4 = 4; // iconst_4; istore 5;
int i5 = 5; // iconst_5; istore 6;
long l0 = 0L; // lconst_0; lstore 7;
long l1 = 1L; // lconst_1; lstore 9;
float f0 = 0F; // fconst_0; fstore 11;
float f1 = 1F; // fconst_1; fstore 12;
float f2 = 2F; // fconst_2; fstore 13;
double d0 = 0D; // dconst_0; dstore 14;
double d1 = 1D; // dconst_1; dstore 16;
int i127 = 127; // bipush 127; istore 18;
int i128 = 128; // sipush 128; istore 19;
Object obj = null; // aconst_null; astore 20;
float f520 = 5.20f; // ldc #2 <5.2>; fstore 21;
String name = "tiemao"; // ldc #3 <tiemao>; astore 22;
long l65536 = 65536L; // ldc2_w #4 <65536>; lstore 23;
double d86400 = 86400.0D; // ldc2_w #6 <86400.0>; dstore 25;
double d00 = 0.0D; // dconst_0; dstore 27;
}
public static void main(String[] args) {
testConstOpcode();
}
}
可以看到, 定义一个变量并赋值常量字面量, 会涉及到2个操作: 常量值入栈, 以及将栈顶元素出栈并存储到局部变量表中的槽位;
下文会详细介绍赋值相关的指令。 为了方便理解, 这里简单说一下:
istore_0
表示将栈顶的int值弹出, 保存到局部变量表的第0号槽位。istore 4
表示将栈顶的int值弹出, 保存到局部变量表的第4号槽位。lstore 7
表示将栈顶的long值弹出, 保存到局部变量表的第7号槽位; 注意long值会在局部变量表中占2个槽位。fstore 11
表示将栈顶的float值弹出, 保存到局部变量表的第11号槽位。dstore 14
表示将栈顶的double值弹出, 保存到局部变量表的第14号槽位; 注意double值也会在局部变量表中占2个槽位。astore 20
表示将栈顶的引用地址(address)弹出, 保存到局部变量表的第20号槽位。
其他的store指令也可以进行类似的理解。
我们可以通过以下命令进行编译和反编译以验证:
# 查看JDK工具的帮助信息
javac -help
javap -help
# 带调试信息编译
javac -g DemoConstantsOpcode.java
# 反编译
javap -v DemoConstantsOpcode.class
# 因为带了package, 所以执行时需要注意路径:
cd ../../..
java com.cncounter.opcode.DemoConstantsOpcode
反编译工具 javap 输出的字节码信息很多, 节选出我们最关心的 testConstOpcode 方法部分:
public static void testConstOpcode();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=29, args_size=0
0: iconst_m1
1: istore_0
2: iconst_0
3: istore_1
4: iconst_1
5: istore_2
6: iconst_2
7: istore_3
8: iconst_3
9: istore 4
11: iconst_4
12: istore 5
14: iconst_5
15: istore 6
17: lconst_0
18: lstore 7
20: lconst_1
21: lstore 9
23: fconst_0
24: fstore 11
26: fconst_1
27: fstore 12
29: fconst_2
30: fstore 13
32: dconst_0
33: dstore 14
35: dconst_1
36: dstore 16
38: bipush 127
40: istore 18
42: sipush 128
45: istore 19
47: aconst_null
48: astore 20
50: ldc #2 // float 5.2f
52: fstore 21
54: ldc #3 // String tiemao
56: astore 22
58: ldc2_w #4 // long 65536l
61: lstore 23
63: ldc2_w #6 // double 86400.0d
66: dstore 25
68: dconst_0
69: dstore 27
71: return
LineNumberTable:
line 9: 0
line 10: 2
line 11: 4
line 12: 6
line 13: 8
line 14: 11
line 15: 14
line 17: 17
line 18: 20
line 19: 23
line 20: 26
line 21: 29
line 22: 32
line 23: 35
line 25: 38
line 26: 42
line 28: 47
line 29: 50
line 30: 54
line 31: 58
line 32: 63
line 33: 68
line 34: 71
LocalVariableTable:
Start Length Slot Name Signature
2 70 0 m1 I
4 68 1 i0 I
6 66 2 i1 I
8 64 3 i2 I
11 61 4 i3 I
14 58 5 i4 I
17 55 6 i5 I
20 52 7 l0 J
23 49 9 l1 J
26 46 11 f0 F
29 43 12 f1 F
32 40 13 f2 F
35 37 14 d0 D
38 34 16 d1 D
42 30 18 i127 I
47 25 19 i128 I
50 22 20 obj Ljava/lang/Object;
54 18 21 f520 F
58 14 22 name Ljava/lang/String;
63 9 23 l65536 J
68 4 25 d86400 D
71 1 27 d00 D
因为我们在javac编译时指定了 -g
参数, 生成详细的调试信息, 所以 javap 能看到行号映射表(LineNumberTable), 以及详细的局部变量表信息(LocalVariableTable)。
简单参考一下即可, 重点关注本节介绍的指令, 暂时不进行详细的讲解。
取值操作(Load)是指从局部变量或者数组元素之中取值, 然后压入操作数栈的栈顶。
Load也可以称为加载。
对应的操作码指令如下:
十进制 | 十六进制 | 助记符 | 说明 |
---|---|---|---|
21 | (0x15) | iload | 从局部变量表槽位中将int值压入操作数栈 |
22 | (0x16) | lload | 从局部变量表槽位中将long值压入操作数栈 |
23 | (0x17) | fload | 从局部变量表槽位中将float值压入操作数栈 |
24 | (0x18) | dload | 从局部变量表槽位中将double值压入操作数栈 |
25 | (0x19) | aload | 从局部变量表槽位中将引用address值压入操作数栈 |
26 | (0x1a) | iload_0 | 将局部变量表0号槽位中的int值压入操作数栈 |
27 | (0x1b) | iload_1 | 将局部变量表1号槽位中的int值压入操作数栈 |
28 | (0x1c) | iload_2 | 将局部变量表2号槽位中的int值压入操作数栈 |
29 | (0x1d) | iload_3 | 将局部变量表3号槽位中的int值压入操作数栈 |
30 | (0x1e) | lload_0 | 将局部变量表0号槽位中的long值压入操作数栈 |
31 | (0x1f) | lload_1 | 将局部变量表1号槽位中的long值压入操作数栈 |
32 | (0x20) | lload_2 | 将局部变量表2号槽位中的long值压入操作数栈 |
33 | (0x21) | lload_3 | 将局部变量表3号槽位中的long值压入操作数栈 |
34 | (0x22) | fload_0 | 将局部变量表0号槽位中的float值压入操作数栈 |
35 | (0x23) | fload_1 | 将局部变量表1号槽位中的float值压入操作数栈 |
36 | (0x24) | fload_2 | 将局部变量表2号槽位中的float值压入操作数栈 |
37 | (0x25) | fload_3 | 将局部变量表3号槽位中的float值压入操作数栈 |
38 | (0x26) | dload_0 | 将局部变量表0号槽位中的double值压入操作数栈 |
39 | (0x27) | dload_1 | 将局部变量表0号槽位中的double值压入操作数栈 |
40 | (0x28) | dload_2 | 将局部变量表0号槽位中的double值压入操作数栈 |
41 | (0x29) | dload_3 | 将局部变量表0号槽位中的double值压入操作数栈 |
42 | (0x2a) | aload_0 | 将局部变量表0号槽位中的引用类型addreass压入操作数栈 |
43 | (0x2b) | aload_1 | 将局部变量表1号槽位中的引用类型addreass压入操作数栈 |
44 | (0x2c) | aload_2 | 将局部变量表2号槽位中的引用类型addreass压入操作数栈 |
45 | (0x2d) | aload_3 | 将局部变量表3号槽位中的引用类型addreass压入操作数栈 |
46 | (0x2e) | iaload | 将int[]数组(array)指定下标位置的值压入操作数栈 |
47 | (0x2f) | laload | 将long[]数组指定下标位置的值压入操作数栈 |
48 | (0x30) | faload | 将float[]数组指定下标位置的值压入操作数栈 |
49 | (0x31) | daload | 将double[]数组指定下标位置的值压入操作数栈 |
50 | (0x32) | aaload | 将引用类型数组指定下标位置的值压入操作数栈 |
51 | (0x33) | baload | 将boolean[]或者byte[]数组指定下标位置的值压入操作数栈 |
52 | (0x34) | caload | 将char[]数组指定下标位置的值压入操作数栈 |
53 | (0x35) | saload | 将short[]数组指定下标位置的值压入操作数栈 |
都是load相关的指令, 都是相同的套路,也很容易记忆。
下面我们构造一段代码, 用来演示这些指令, 个别的可能涵盖不到, 为了简单就不强行构造了, 读者照搬套路即可:
package com.cncounter.opcode;
import java.util.Arrays;
/**
* 演示常量相关的操作码; 这些方法纯粹是为了演示;
*/
public class DemoLoadOpcode {
public static void testIntLoad(int num0, int num1, int num2,
int num3, int num4) {
// 方法的每个int参数占一个槽位
// iload_0; iload_1; iadd; iload_2; iadd;
// iload_3; iadd; iload 4; iadd; istore 5;
int total = num0 + num1 + num2 + num3 + num4;
// 所以 total 排到第5号槽位
// iload 5; iload 5;
Integer.valueOf(total);
}
public static void testLongLoad(long num0, long num1, long num2) {
// 每个 long 型入参占2个槽位
// lload_0; lload_2; ladd; lload 4; ladd;
Long.valueOf(num0 + num1 + num2);
}
public void testInstanceLongLoad(long num1, long num2) {
// 实例方法中, 局部变量表的0号槽位被 this 占了
// 然后是方法入参, 每个long占2个槽位
// aload_0; lload_1; l2d; lload_3; l2d;
this.testInstanceDoubleLoad(num1, num2);
}
public static void testFloatLoad(float num0, float num1, float num2,
float num3, float num4) {
// fload_0; fload_1; fadd; fload_2; fadd;
// fload_3; fadd; fload 4; fadd;
Float.valueOf(num0 + num1 + num2 + num3 + num4);
}
public static void testDoubleLoad(double num0, double num1, double num2) {
// 每个 double 型入参占2个槽位
// dload_0; dload_2; dadd; dload 4; dadd;
Double.valueOf(num0 + num1 + num2);
}
// FIXME: 这是一个死循环递归方法, 此处仅用于演示
public void testInstanceDoubleLoad(double num1, double num2) {
// 实例方法, 局部变量表的0号槽位同来存放 this
// aload_0; dload_1; dload_3;
testInstanceDoubleLoad(num1, num2);
}
public static void testReferenceAddrLoad(String str0, Object obj1, Integer num2,
Long num3, Float num4, Double num5) {
// 方法每个 obj 参数占一个槽位; 部分字节码:
// aload_0; aload_1; aload_2; aload_3; aload 4; aload 5
Arrays.asList(str0, obj1, num2, num3, num4, num5);
}
public static void testArrayLoad(int[] array0, long[] array1, float[] array2,
double[] array3, String[] array4, boolean[] array5,
byte[] array6, char[] array7, short[] array8) {
// 这几个操作的字节码套路都是一样的:
// 数组引用; 下标; 数组取值; 赋值给局部变量;
// aload_0; iconst_0; iaload; istore 9;
int num0 = array0[0];
// aload_1; iconst_1; laload; lstore 10;
long num1 = array1[1];
// aload_2; iconst_2; faload; fstore 12;
float num2 = array2[2];
// aload_3; iconst_3; daload; dstore 13;
double num3 = array3[3];
// aload 4; iconst_4; aaload; astore 15;
Object obj4 = array4[4];
// aload 5; iconst_5; baload; istore 16;
boolean bool5 = array5[5];
// aload 6; bipush 6; baload; istore 17;
byte byte6 = array6[6];
// aload 7; bipush 7; caload; istore 18;
char char7 = array7[7];
// aload 8; bipush 8; saload; istore 19;
short num8 = array8[8];
}
public static void main(String[] args) {
}
}
字节码中, 每个指令后面附带的操作数, 其含义由操作码不同而不同, 分析时需要辨别。
其实代码中的注释信息已经很明确了。
我们先编译和反编译代码。
# 带调试信息编译
javac -g DemoLoadOpcode.java
# 反编译
javap -v DemoLoadOpcode.class
反编译之后查看到的字节码信息很多, 套路都是差不多的, 读者可以快速看一遍, 简单过一遍即可。
一个一个来看。
这个方法演示从局部变量表取int值的指令。
关键代码是:
int total = num0 + num1 + num2 + num3 + num4;
反编译后的字节码信息为:
public static void testIntLoad(int, int, int, int, int);
descriptor: (IIIII)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=5
0: iload_0
1: iload_1
2: iadd
3: iload_2
4: iadd
5: iload_3
6: iadd
7: iload 4
9: iadd
10: istore 5
12: iload 5
14: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: pop
18: return
LineNumberTable:
line 15: 0
line 18: 12
line 19: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 num0 I
0 19 1 num1 I
0 19 2 num2 I
0 19 3 num3 I
0 19 4 num4 I
12 7 5 total I
和代码中的注释信息进行对照和验证。 可以发现套路都差不多, 记住1个就记住了5个。
解读如下:
iload_0
;iload_1
;iload_2
;iload_3
;iload 4
; 从对应的槽位加载int值。iadd
; 执行int相加; 消耗2个操作数栈中的int值, 压入一个int值。istore 5
; 前面介绍过, 将栈顶int值弹出并保存到局部变量表的 5 号槽位中。
这个方法演示从局部变量表取long值的指令。
关键代码是:
Long.valueOf(num0 + num1 + num2);
反编译后的字节码信息为:
public static void testLongLoad(long, long, long);
descriptor: (JJJ)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=3
0: lload_0
1: lload_2
2: ladd
3: lload 4
5: ladd
6: invokestatic #3 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
9: pop
10: return
LineNumberTable:
line 24: 0
line 25: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 num0 J
0 11 2 num1 J
0 11 4 num2 J
解读如下:
- 每个 long 类型的占2个槽位, 所以3个long类型入参占据了0号,2号,4号槽位;
lload_0
从0号槽位取值;lload_2
从2号槽位取值;lload 4
从4号槽位取值。pop
则是因为我们调用的Long.valueOf
方法有返回值, 这里没用到, 所以要扔掉, 也就是从操作数栈中弹出.
那么如何从1号和3号槽位取long类型的值呢?
这个方法演示从局部变量表取long值的指令, 注意这不是 static 方法, 而是一个实例方法。
关键代码是:
this.testInstanceDoubleLoad(num1, num2);
可以看到, 内部调用了另一个实例方法。
反编译后的字节码信息为:
public void testInstanceLongLoad(long, long);
descriptor: (JJ)V
flags: ACC_PUBLIC
Code:
stack=5, locals=5, args_size=3
0: aload_0
1: lload_1
2: l2d
3: lload_3
4: l2d
5: invokevirtual #4 // Method testInstanceDoubleLoad:(DD)V
8: return
LineNumberTable:
line 31: 0
line 32: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/cncounter/opcode/DemoLoadOpcode;
0 9 1 num1 J
0 9 3 num2 J
解读如下:
aload_0
加载0号槽位的引用, 也就是this指针。lload_1
加载1号槽位的long值, 这里就是第一个方法入参。lload_3
加载3号槽位的long值, 因为前一个局部变量(方法入参)是long, 所以不存在2号槽位。l2d
是执行类型转换的, 学习Java基础时, 我们就知道long允许自动转型为 double。invokevirtual
是执行普通的实例方法。
这个方法演示从局部变量表取float值的指令。
关键代码是:
Float.valueOf(num0 + num1 + num2 + num3 + num4);
反编译后的字节码信息为:
public static void testFloatLoad(float, float, float, float, float);
descriptor: (FFFFF)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=5
0: fload_0
1: fload_1
2: fadd
3: fload_2
4: fadd
5: fload_3
6: fadd
7: fload 4
9: fadd
10: invokestatic #5 // Method java/lang/Float.valueOf:(F)Ljava/lang/Float;
13: pop
14: return
LineNumberTable:
line 38: 0
line 39: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 num0 F
0 15 1 num1 F
0 15 2 num2 F
0 15 3 num3 F
0 15 4 num4 F
解读如下:
fload_0
;fload_1
;fload_2
;fload_3
;fload 4
; 分别从各个槽位取float值, 压入栈顶。fadd
; 浮点数相加;pop
: 我们调用的方法有返回值, 却没用到, 所以要从操作数栈中弹出.
这个方法演示从局部变量表取 double 值的指令。
关键代码是:
Double.valueOf(num0 + num1 + num2);
反编译后的字节码信息为:
public static void testDoubleLoad(double, double, double);
descriptor: (DDD)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=3
0: dload_0
1: dload_2
2: dadd
3: dload 4
5: dadd
6: invokestatic #6 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
9: pop
10: return
LineNumberTable:
line 44: 0
line 45: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 num0 D
0 11 2 num1 D
0 11 4 num2 D
解读如下:
dload_0
从局部变量表的0号槽位取double值dload_2
从局部变量表的2号槽位取double值dload 4
从局部变量表的4号槽位取double值dadd
执行double值相加invokestatic
执行静态方法;
这个方法演示从局部变量表取 double 值的指令。 注意这是一个实例方法。
关键代码是:
Double.valueOf(num0 + num1 + num2);
反编译后的字节码信息为:
public void testInstanceDoubleLoad(double, double);
descriptor: (DD)V
flags: ACC_PUBLIC
Code:
stack=5, locals=5, args_size=3
0: aload_0
1: dload_1
2: dload_3
3: invokevirtual #4 // Method testInstanceDoubleLoad:(DD)V
6: return
LineNumberTable:
line 51: 0
line 52: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/cncounter/opcode/DemoLoadOpcode;
0 7 1 num1 D
0 7 3 num2 D
解读如下:
aload_0
加载0号槽位的引用, 也就是this指针。dload_1
加载1号槽位的double值, 这里就是第一个方法入参。dload_3
加载3号槽位的double值, 因为前一个局部变量(方法入参)是double, 所以不存在2号槽位。invokevirtual
是执行普通的实例方法。
这个方法演示从局部变量表取对象引用地址的指令。
关键代码是:
Arrays.asList(str0, obj1, num2, num3, num4, num5);
反编译后的字节码信息为:
public static void testReferenceAddrLoad
(java.lang.String, java.lang.Object, java.lang.Integer,
java.lang.Long, java.lang.Float, java.lang.Double);
descriptor: (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Integer;
Ljava/lang/Long;Ljava/lang/Float;Ljava/lang/Double;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=6
0: bipush 6
2: anewarray #7 // class java/lang/Object
5: dup
6: iconst_0
7: aload_0
8: aastore
9: dup
10: iconst_1
11: aload_1
12: aastore
13: dup
14: iconst_2
15: aload_2
16: aastore
17: dup
18: iconst_3
19: aload_3
20: aastore
21: dup
22: iconst_4
23: aload 4
25: aastore
26: dup
27: iconst_5
28: aload 5
30: aastore
31: invokestatic #8 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
34: pop
35: return
LineNumberTable:
line 58: 0
line 59: 35
LocalVariableTable:
Start Length Slot Name Signature
0 36 0 str0 Ljava/lang/String;
0 36 1 obj1 Ljava/lang/Object;
0 36 2 num2 Ljava/lang/Integer;
0 36 3 num3 Ljava/lang/Long;
0 36 4 num4 Ljava/lang/Float;
0 36 5 num5 Ljava/lang/Double;
这里进行了一点点折行排版。不影响我们理解。
解读如下:
aload_0
;aload_1
;aload_2
;aload_3
;aload 4
;aload 5
; 这几个指令是从局部变量表槽位中获取引用地址值。- 具体是什么引用类型不重要, 在字节码文件中都使用32位存储。
Arrays.asList
有点特殊, 接收的是动态参数:public static <T> List<T> asList(T... a)
; 所以编译器会自动将这些参数转换为一个对象数组。anewarray #7 // class java/lang/Object
。iconst_0
到iconst_5
这些指令主要是构造数组的下标。aastore
就是根据栈中的参数, 保存到对象数组之中(address array store).dup
则是将栈顶元素复制一份并入栈。
这个方法演示从各种类型的数组中取值。
部分关键代码是:
// ......
int num0 = array0[0];
// ......
Object obj4 = array4[4];
// ......
反编译后的字节码信息为:
public static void testArrayLoad(int[], long[],
float[], double[], java.lang.String[],
boolean[], byte[], char[], short[]);
descriptor: ([I[J[F[D[Ljava/lang/String;[Z[B[C[S)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=20, args_size=9
0: aload_0
1: iconst_0
2: iaload
3: istore 9
5: aload_1
6: iconst_1
7: laload
8: lstore 10
10: aload_2
11: iconst_2
12: faload
13: fstore 12
15: aload_3
16: iconst_3
17: daload
18: dstore 13
20: aload 4
22: iconst_4
23: aaload
24: astore 15
26: aload 5
28: iconst_5
29: baload
30: istore 16
32: aload 6
34: bipush 6
36: baload
37: istore 17
39: aload 7
41: bipush 7
43: caload
44: istore 18
46: aload 8
48: bipush 8
50: saload
51: istore 19
53: return
LineNumberTable:
line 67: 0
line 69: 5
line 71: 10
line 73: 15
line 75: 20
line 77: 26
line 79: 32
line 81: 39
line 83: 46
line 84: 53
LocalVariableTable:
Start Length Slot Name Signature
0 54 0 array0 [I
0 54 1 array1 [J
0 54 2 array2 [F
0 54 3 array3 [D
0 54 4 array4 [Ljava/lang/String;
0 54 5 array5 [Z
0 54 6 array6 [B
0 54 7 array7 [C
0 54 8 array8 [S
5 49 9 num0 I
10 44 10 num1 J
15 39 12 num2 F
20 34 13 num3 D
26 28 15 obj4 Ljava/lang/Object;
32 22 16 bool5 Z
39 15 17 byte6 B
46 8 18 char7 C
53 1 19 num8 S
这段代码稍微有点长。
简单解读一下:
aload_0
直到aload 8
这些指令, 从局部变量表的0到8号槽位取值, 这里就是取不同的入参。iconst_0
到iconst_5
, 以及bipush 8
, 对应我们在代码里面写的数组下标值。laload; faload; daload; aaload; baload; baload; caload; saload;
这几个指令就是从不同的数组中取值;
再来看看我们的代码和注释会更容易理解一些:
// 这几个操作的字节码套路都是一样的:
// 数组引用; 下标; 数组取值; 赋值给局部变量;
// aload_0; iconst_0; iaload; istore 9;
int num0 = array0[0];
// aload_1; iconst_1; laload; lstore 10;
long num1 = array1[1];
// aload_2; iconst_2; faload; fstore 12;
float num2 = array2[2];
// aload_3; iconst_3; daload; dstore 13;
double num3 = array3[3];
// aload 4; iconst_4; aaload; astore 15;
Object obj4 = array4[4];
// aload 5; iconst_5; baload; istore 16;
boolean bool5 = array5[5];
// aload 6; bipush 6; baload; istore 17;
byte byte6 = array6[6];
// aload 7; bipush 7; caload; istore 18;
char char7 = array7[7];
// aload 8; bipush 8; saload; istore 19;
short num8 = array8[8];
赋值操作(Store)是指将操作数栈栈顶的元素弹出, 并赋值给局部变量或者数组元素。
Store也可以称为保存, 本质是将CPU寄存器中的值保存到主内存中。 当然这里面存在一些映射和缓存关系,比如 "操作数栈 -- 数据寄存器", "CPU高速缓存 -- 内存/方法栈/局部变量表" 等等。
Store对应的操作码指令如下:
十进制 | 十六进制 | 助记符 | 说明 |
---|---|---|---|
54 | (0x36) | istore | 将操作数栈栈顶的int值弹出并保存到局部变量表槽位 |
55 | (0x37) | lstore | 将栈顶的long值弹出并保存到局部变量表槽位 |
56 | (0x38) | fstore | 将栈顶的float值弹出并保存到局部变量表槽位 |
57 | (0x39) | dstore | 将栈顶的double值弹出并保存到局部变量表槽位 |
58 | (0x3a) | astore | 将栈顶的对象引用的address值弹出并保存到局部变量表槽位 |
59 | (0x3b) | istore_0 | 将栈顶的int值弹出并保存到第0号局部变量表槽位 |
60 | (0x3c) | istore_1 | 将栈顶的int值弹出并保存到第1号局部变量表槽位 |
61 | (0x3d) | istore_2 | 将栈顶的int值弹出并保存到第2号局部变量表槽位 |
62 | (0x3e) | istore_3 | 将栈顶的int值弹出并保存到第3号局部变量表槽位 |
63 | (0x3f) | lstore_0 | 将栈顶的long值弹出并保存到第0号局部变量表槽位 |
64 | (0x40) | lstore_1 | 将栈顶的long值弹出并保存到第1号局部变量表槽位 |
65 | (0x41) | lstore_2 | 将栈顶的long值弹出并保存到第2号局部变量表槽位 |
66 | (0x42) | lstore_3 | 将栈顶的long值弹出并保存到第3号局部变量表槽位 |
67 | (0x43) | fstore_0 | 将栈顶的float值弹出并保存到第0号局部变量表槽位 |
68 | (0x44) | fstore_1 | 将栈顶的float值弹出并保存到第1号局部变量表槽位 |
69 | (0x45) | fstore_2 | 将栈顶的float值弹出并保存到第2号局部变量表槽位 |
70 | (0x46) | fstore_3 | 将栈顶的float值弹出并保存到第3号局部变量表槽位 |
71 | (0x47) | dstore_0 | 将栈顶的double值弹出并保存到第0号局部变量表槽位 |
72 | (0x48) | dstore_1 | 将栈顶的double值弹出并保存到第1号局部变量表槽位 |
73 | (0x49) | dstore_2 | 将栈顶的double值弹出并保存到第2号局部变量表槽位 |
74 | (0x4a) | dstore_3 | 将栈顶的double值弹出并保存到第3号局部变量表槽位 |
75 | (0x4b) | astore_0 | 将栈顶的对象引用address值弹出并保存到第0号局部变量表槽位 |
76 | (0x4c) | astore_1 | 将栈顶的对象引用address值弹出并保存到第1号局部变量表槽位 |
77 | (0x4d) | astore_2 | 将栈顶的对象引用address值弹出并保存到第2号局部变量表槽位 |
78 | (0x4e) | astore_3 | 将栈顶的对象引用address值弹出并保存到第3号局部变量表槽位 |
79 | (0x4f) | iastore | 将栈顶的int值弹出并保存到数组的指定下标位置 |
80 | (0x50) | lastore | 将栈顶的long值弹出并保存到数组的指定下标位置 |
81 | (0x51) | fastore | 将栈顶的float值弹出并保存到数组的指定下标位置 |
82 | (0x52) | dastore | 将栈顶的double值弹出并保存到数组的指定下标位置 |
83 | (0x53) | aastore | 将栈顶的对象引用address值弹出并保存到数组的指定下标位置 |
84 | (0x54) | bastore | 将栈顶的值弹出并当做 byte/或boolean值 保存到数组的指定下标位置 |
85 | (0x55) | castore | 将栈顶的char值弹出并保存到数组的指定下标位置 |
86 | (0x56) | sastore | 将栈顶的short值弹出并保存到数组的指定下标位置 |
store相关的指令, 和load部分的指令基本上一一对应, 相同的套路,很容易记忆。
// TODO
栈(Stack)操作符干的事情, 就是纯粹对操作数栈内部进行操作。
操作数栈, 是方法帧内部的一个数据结构, 也简称 "栈"。
Stack在这里明显是指的操作数栈。
栈操作相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 效果说明 |
---|---|---|---|
87 | (0x57) | pop | 弹出栈顶的1个32bit操作数 |
88 | (0x58) | pop2 | 弹出栈顶的2个32bit操作数(或者1个64bit操作数) |
89 | (0x59) | dup | 复制1个栈顶的32bit操作数,并压入栈顶 |
90 | (0x5a) | dup_x1 | 复制1个栈顶的32bit值,x1表示将复制的值跨1个位置, 插入原始值下面的1个32bit值的下方 |
91 | (0x5b) | dup_x2 | 复制1个栈顶的32bit值,x2表示将复制的值跨2个位置, 插入原始值下面的2个32bit值的下方 |
92 | (0x5c) | dup2 | 复制2个栈顶的32bit值/或1个64bit值,并压入栈顶 |
93 | (0x5d) | dup2_x1 | 复制2个栈顶的32bit值/或1个64bit值,x1表示将复制的值跨1个位置, 按原始顺序,插入原始值下面的1个32bit值的下方 |
94 | (0x5e) | dup2_x2 | 复制2个栈顶的32bit值/或1个64bit值,x2表示将复制的值跨2个位置, 按原始顺序,插入原始值下面的2个32bit值的下方 |
95 | (0x5f) | swap | 将栈顶的2个操作数(32bit)交换位置 |
栈操作相关的指令, 这里给出的是效果说明。
实际进行理解时,可以加入一些中间态。比如:
- swap 实际上是吃掉栈顶的两个操作数, 然后再将他们调换顺序之后, 依次压入栈顶。
// 方法返回值不保存
return this.index++;
https://juejin.cn/post/6844903693083475982
// TODO
数学运算(Math)操作符干的事情, 就是进行算术操作。
一般会吃掉操作数栈栈顶的多个元素,然后再压入一个结果值。
算术操作相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 说明 |
---|---|---|---|
96 | (0x60) | iadd | 将栈顶的2个 int 值取出,相加(Add int),并将结果压入栈顶 |
97 | (0x61) | ladd | 将栈顶的2个 long 值取出,相加,并将结果压入栈顶 |
98 | (0x62) | fadd | 将栈顶的2个 float 值取出,相加,并将结果压入栈顶 |
99 | (0x63) | dadd | 将栈顶的2个 double 值取出,相加,并将结果压入栈顶 |
100 | (0x64) | isub | 将栈顶的2个 int 值取出,相减(次顶 - 栈顶 , Subtract int ),并将结果压入栈顶 |
101 | (0x65) | lsub | 将栈顶的2个 long 值取出,相减(次顶 - 栈顶 ),并将结果压入栈顶 |
102 | (0x66) | fsub | 将栈顶的2个 float 值取出,相减(次顶 - 栈顶 ),并将结果压入栈顶 |
103 | (0x67) | dsub | 将栈顶的2个 double 值取出,相减(次顶 - 栈顶 ),并将结果压入栈顶 |
104 | (0x68) | imul | 将栈顶的2个 int 值取出,相乘(Multiply int),并将结果压入栈顶 |
105 | (0x69) | lmul | 将栈顶的2个 long 值取出,相乘,并将结果压入栈顶 |
106 | (0x6a) | fmul | 将栈顶的2个 float 值取出,相乘,并将结果压入栈顶 |
107 | (0x6b) | dmul | 将栈顶的2个 double 值取出,相乘,并将结果压入栈顶 |
108 | (0x6c) | idiv | 将栈顶的2个 int 值取出,相除(次顶 / 栈顶 , Divide int ),并将结果压入栈顶 |
109 | (0x6d) | ldiv | 将栈顶的2个 long 值取出,相除(次顶 / 栈顶 ),并将结果压入栈顶 |
110 | (0x6e) | fdiv | 将栈顶的2个 float 值取出,相除(次顶 / 栈顶 ),并将结果压入栈顶 |
111 | (0x6f) | ddiv | 将栈顶的2个 double 值取出,相除(次顶 / 栈顶 ),并将结果压入栈顶 |
112 | (0x70) | irem | 将栈顶的2个 int 值取出,取余(次顶 % 栈顶 , Remainder int ), 并将结果压入栈顶 |
113 | (0x71) | lrem | 将栈顶的2个 long 值取出,取余(次顶 % 栈顶 ), 并将结果压入栈顶 |
114 | (0x72) | frem | 将栈顶的2个 float 值取出,取余(次顶 % 栈顶 ), 并将结果压入栈顶 |
115 | (0x73) | drem | 将栈顶的2个 double 值取出,取余(次顶 % 栈顶 ), 并将结果压入栈顶 |
116 | (0x74) | ineg | 将栈顶的1个 int 值取出,算术取负(Negate int, 即 x变为-x ), 并将结果压入栈顶 |
117 | (0x75) | lneg | 将栈顶的1个 long 值取出,算术取负, 并将结果压入栈顶 |
118 | (0x76) | fneg | 将栈顶的1个 float 值取出,算术取负, 并将结果压入栈顶 |
119 | (0x77) | dneg | 将栈顶的1个 double 值取出,算术取负, 并将结果压入栈顶 |
120 | (0x78) | ishl | int值左移操作符; 次顶为iv1,栈顶为x,将这两个数取出, 对iv1左移x位 (x大于32则取模)并将计算结果入栈; |
121 | (0x79) | lshl | long值左移操作符; 次顶为lv1,栈顶为int值x,将这两个数取出, 对lv1左移x位 (x大于64则取模)并将计算结果入栈; |
122 | (0x7a) | ishr | int值右移操作符; 次顶为iv1,栈顶为x,将这两个数取出, 对iv1右移x位 (x大于32则取模)并将计算结果入栈; |
123 | (0x7b) | lshr | long值右移操作符; 次顶为lv1,栈顶为int值x,将这两个数取出, 对lv1右移x位 (x大于64则取模)并将结果入栈; |
124 | (0x7c) | iushr | int值无符号右移操作符? |
125 | (0x7d) | lushr | long值无符号右移操作符? |
126 | (0x7e) | iand | int值按位与操作; 取出2个32bit的int操作数, 按位与计算, 并将结果入栈; |
127 | (0x7f) | land | long值按位与操作; 取出2个64bit的long操作数, 按位与计算, 并将结果入栈; |
128 | (0x80) | ior | int值按位或操作; 取出2个32bit的int操作数, 按位或计算, 并将结果入栈; |
129 | (0x81) | lor | long值按位或操作; 取出2个64bit的long操作数, 按位或计算, 并将结果入栈; |
130 | (0x82) | ixor | int值按位异或操作; 取出2个32bit的int操作数, 按位异或计算(XOR), 并将结果入栈; |
131 | (0x83) | lxor | long值按位异或操作; 取出2个64bit的long操作数, 按位异或计算(XOR), 并将结果入栈; |
132 | (0x84) | iinc | 操作数栈无变化; 将 index 局部变量槽位上的int值递增一个常数值 |
// TODO
类型转换(Conversions)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 效果说明 |
---|---|---|---|
133 | (0x85) | i2l | int转long; 将栈顶的int值弹出,带符号转换为long值并入栈 |
134 | (0x86) | i2f | int转float; 遵循 IEEE 754 浮点数运算标准 |
135 | (0x87) | i2d | int转double; |
136 | (0x88) | l2i | long转int; 将栈顶的long值弹出,舍弃高32位,将低32位当做int值并入栈 |
137 | (0x89) | l2f | long转float; 遵循 IEEE 754 浮点数运算标准 |
138 | (0x8a) | l2d | long转double; 遵循 IEEE 754 浮点数运算标准 |
139 | (0x8b) | f2i | float转int; |
140 | (0x8c) | f2l | float转long; |
141 | (0x8d) | f2d | float转double; |
142 | (0x8e) | d2i | double转int |
143 | (0x8f) | d2l | double转long |
144 | (0x90) | d2f | double转float |
145 | (0x91) | i2b | int转byte; 操作数栈中还是使用32bit存储byte值 |
146 | (0x92) | i2c | int转char |
147 | (0x93) | i2s | int转short |
// TODO
比较(Comparisons)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 效果说明 |
---|---|---|---|
148 | (0x94) | lcmp | long值比较; 和java的compare规则类型, (次顶 - 栈顶;转换为 1,0,-1) |
149 | (0x95) | fcmpl | float值比较; 将NaN当做less |
150 | (0x96) | fcmpg | float值比较; 将NaN当做greater |
151 | (0x97) | dcmpl | double值比较; 将NaN当做less |
152 | (0x98) | dcmpg | double值比较; 将NaN当做greater |
153 | (0x99) | ifeq | 比较前一操作的结果决定分支(判断栈顶值等于0;) |
154 | (0x9a) | ifne | 比较前一操作的结果决定分支(判断栈顶值不等于0;) |
155 | (0x9b) | iflt | 比较前一操作的结果决定分支(判断栈顶值小于0;) |
156 | (0x9c) | ifge | 比较前一操作的结果决定分支(判断栈顶值大于等于0;) |
157 | (0x9d) | ifgt | 比较前一操作的结果决定分支(判断栈顶值大于0;) |
158 | (0x9e) | ifle | 比较前一操作的结果决定分支(判断栈顶值小于等于0;) |
159 | (0x9f) | if_icmpeq | 比较栈顶的2个int值决定分支;(判断两个值相等;) |
160 | (0xa0) | if_icmpne | 比较栈顶的2个int值决定分支;(判断两个值不相等;) |
161 | (0xa1) | if_icmplt | 比较栈顶的2个int值决定分支;(判断次顶值小于栈顶值;) |
162 | (0xa2) | if_icmpge | 比较栈顶的2个int值决定分支;(判断次顶值大于等于栈顶值;) |
163 | (0xa3) | if_icmpgt | 比较栈顶的2个int值决定分支;(判断次顶值大于栈顶值;) |
164 | (0xa4) | if_icmple | 比较栈顶的2个int值决定分支;(判断次顶值小于等于栈顶值;) |
165 | (0xa5) | if_acmpeq | 比较栈顶的2个地址引用决定分支;(判断两个地址相等;) |
166 | (0xa6) | if_acmpne | 比较栈顶的2个地址引用决定分支;(判断两个地址不相等;) |
// TODO
流程控制(Control)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 附带操作数(字节) | 出栈 | 入栈 | 效果说明 |
---|---|---|---|---|---|---|
167 | (0xa7) | goto | 2 | 0 | 0 | 跳转指令; |
200 | (0xc8) | goto_w | 4 | 0 | 0 | 跳转指令, 宽索引; |
168 | (0xa8) | jsr | 2 | 0 | 0 | 跳转子路由; Jump subroutine; 比如try-finally |
201 | (0xc9) | jsr_w | 4 | 0 | 0 | 跳转子路由; 带4字节的指令操作数 |
169 | (0xa9) | ret | 1 | 0 | 0 | 从子路由返回; |
170 | (0xaa) | tableswitch | 变长 | 2 | 0 | 表格式 switch 跳转; |
171 | (0xab) | lookupswitch | 变长 | 2 | 0 | 查找式 switch 跳转; |
172 | (0xac) | ireturn | 0 | [清空] | 0 | 返回 int 能表示的栈顶值 |
173 | (0xad) | lreturn | 0 | [清空] | 0 | 返回 long 类型的栈顶值 |
174 | (0xae) | freturn | 0 | [清空] | 0 | 返回 float 类型的栈顶值 |
175 | (0xaf) | dreturn | 0 | [清空] | 0 | 返回 double 类型的栈顶值 |
176 | (0xb0) | areturn | 0 | [清空] | 0 | 返回地址引用类型的栈顶值 |
177 | (0xb1) | return | 0 | [清空] | 0 | 返回 void |
// TODO
对象引用(References)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 附带操作数(字节) | 出栈 | 入栈 | 效果说明 |
---|---|---|---|---|---|---|
178 | (0xb2) | getstatic | 2 | 0 | 1 | 获取static字段值并入栈, 由附带操作数确定常量池中的具体字段 |
179 | (0xb3) | putstatic | 2 | 1 | 0 | 将栈顶值写入static字段 |
180 | (0xb4) | getfield | 2 | 1 | 1 | 获取对象引用的实例属性值并入栈 |
181 | (0xb5) | putfield | 2 | 2 | 0 | 将栈顶值写入对象引用的实例属性域 |
182 | (0xb6) | invokevirtual | 2 | 1+ | 0 | 调用对象的实例方法 |
183 | (0xb7) | invokespecial | 2 | 1+ | 0 | 调用对象的特殊实例方法; 如构造函数、超类方法,以及private |
184 | (0xb8) | invokestatic | 2 | 0+ | 0 | 调用静态方法 |
185 | (0xb9) | invokeinterface | 4 | 1+ | 0 | 调用接口方法 |
186 | (0xba) | invokedynamic | 4 | 0+ | 0 | 动态方法调用 |
187 | (0xbb) | new | 2 | 0 | 1 | 创建新对象; 注意不是调用构造方法; |
188 | (0xbc) | newarray | 1 | 1 | 1 | 创建新数组 |
189 | (0xbd) | anewarray | 2 | 1 | 1 | 创建新的对象数组 |
190 | (0xbe) | arraylength | 0 | 1 | 1 | 获取数组长度 |
191 | (0xbf) | athrow | 0 | 1 | 特殊 | 抛出异常或错误 |
192 | (0xc0) | checkcast | 2 | 1 | 1 | 类型强转 |
193 | (0xc1) | instanceof | 2 | 1 | 1 | 判断对象是否属于给定类型 |
194 | (0xc2) | monitorenter | 0 | 1 | 0 | 进入对象锁范围 |
195 | (0xc3) | monitorexit | 0 | 1 | 0 | 退出对象锁范围 |
下面我们通过实际的例子, 进行详细介绍。
// TODO
请看代码:
package com.cncounter.opcode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 演示invoke操作码
*/
public class DemoInvokeOpcode {
public static void testMethodInvoke() {
// 183; invokespecial
HashMap<String, String> hashMap = new HashMap<String, String>(100);
// 182; invokevirtual
hashMap.put("name", "tiemao");
// 赋值给Map接口引用
Map<String, String> map = hashMap;
// 185; invokeinterface
map.putIfAbsent("url", "https://renfufei.blog.csdn.net");
// 使用lambda
List<String> upperKeys = map.keySet().stream()
// 186; invokedynamic
.map(i -> i.toUpperCase())
.collect(Collectors.toList());
// 184; invokestatic
String str = String.valueOf(upperKeys);
// 182; invokevirtual
System.out.println(str);
}
public static void main(String[] args) {
// 184; invokestatic
testMethodInvoke();
}
}
执行main方法之后的输出内容为:
[NAME, URL]
我们可以使用以下命令进行编译和反编译:
# 查看JDK工具的帮助信息
javac -help
javap -help
# 带调试信息编译
javac -g DemoInvokeOpcode.java
# 反编译
javap -v DemoInvokeOpcode.class
# 因为带了package, 所以执行时需要注意路径:
cd ../../..
java com.cncounter.opcode.DemoInvokeOpcode
javac编译之后, 可以看到只生成了一个文件 DemoInvokeOpcode.class
。 这也是 lambda 与内部类不同的地方。
反编译工具 javap 输出的字节码信息很多, 节选出我们最关心的testMethodInvoke方法部分:
public static void testMethodInvoke();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=0
0: new #2 // class java/util/HashMap
3: dup
4: bipush 100
6: invokespecial #3 // Method java/util/HashMap."<init>":(I)V
9: astore_0
10: aload_0
11: ldc #4 // String name
13: ldc #5 // String tiemao
15: invokevirtual #6 // Method java/util/HashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
18: pop
19: aload_0
20: astore_1
21: aload_1
22: ldc #7 // String url
24: ldc #8 // String https://renfufei.blog.csdn.net
26: invokeinterface #9, 3 // InterfaceMethod java/util/Map.putIfAbsent:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
31: pop
32: aload_1
33: invokeinterface #10, 1 // InterfaceMethod java/util/Map.keySet:()Ljava/util/Set;
38: invokeinterface #11, 1 // InterfaceMethod java/util/Set.stream:()Ljava/util/stream/Stream;
43: invokedynamic #12, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
48: invokeinterface #13, 2 // InterfaceMethod java/util/stream/Stream.map:(Ljava/util/function/Function;)Ljava/util/stream/Stream;
53: invokestatic #14 // Method java/util/stream/Collectors.toList:()Ljava/util/stream/Collector;
56: invokeinterface #15, 2 // InterfaceMethod java/util/stream/Stream.collect:(Ljava/util/stream/Collector;)Ljava/lang/Object;
61: checkcast #16 // class java/util/List
64: astore_2
65: aload_2
66: invokestatic #17 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
69: astore_3
70: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
73: aload_3
74: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
77: return
LineNumberTable:
line 15: 0
line 17: 10
line 19: 19
line 21: 21
line 23: 32
line 25: 48
line 26: 53
line 28: 65
line 30: 70
line 31: 77
LocalVariableTable:
Start Length Slot Name Signature
10 68 0 hashMap Ljava/util/HashMap;
21 57 1 map Ljava/util/Map;
65 13 2 upperKeys Ljava/util/List;
70 8 3 str Ljava/lang/String;
LocalVariableTypeTable:
Start Length Slot Name Signature
10 68 0 hashMap Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;
21 57 1 map Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
65 13 2 upperKeys Ljava/util/List<Ljava/lang/String;>;
简单解释如下:
- 调用某个类的静态方法, 使用的是 invokestatic 指令。
- 当通过接口引用来调用方法时, 会直接编译为 invokeinterface 指令。
- 调用构造函数会编译为 invokespecial 指令, 当然还包括调用 private 方法, 以及可见的超类方法。
- 如果变量引用的类型是具体类, 则编译器会使用 invokevirtual 来调用 public, protected和包可见级别的方法。
- JDK7新增加了一个
invokedynamic
指令, 用来支持“动态类型语言”(Dynamically TypedLanguage, 从JDK8开始引入的lambda表达式, 在使用时会编译为这个指令。
多维数组;
扩展(Extended)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 附带操作数(字节) | 出栈 | 入栈 | 效果说明 |
---|---|---|---|---|---|---|
196 | (0xc4) | wide | 5? | 看具体指令 | 看具体指令 | 宽索引指令; 扩展指令的操作数索引范围 |
197 | (0xc5) | multianewarray | 3 | 看具体维数 | 1 | 创建多维数组 |
198 | (0xc6) | ifnull | 2 | 1 | 0 | 栈顶引用为null则跳转; |
199 | (0xc7) | ifnonnull | 0 | 0 | 0 | 栈顶引用不为null则跳转; |
200 | (0xc8) | goto_w | 4 | 0 | 0 | goto跳转指令, 宽索引; |
201 | (0xc9) | jsr_w | 4 | 0 | 0 | jsr跳转子路由, 宽索引; |
// TODO
保留(Reserved)操作符。
相关的操作码指令如下:
十进制 | 十六进制 | 助记符 | 附带操作数(字节) | 出栈 | 入栈 | 效果说明 |
---|---|---|---|---|---|---|
202 | (0xca) | breakpoint | ? | ? | ? | 供调试器用于实现断点 |
254 | (0xfe) | impdep1 | 0 | 0 | 0 | 依赖于具体实现的后门指令 |
255 | (0xff) | impdep2 | 0 | 0 | 0 | 依赖于具体实现的后门指令 |
依赖于特定JVM平台的实现, 一般在class文件中不会出现, 只能在 Java 虚拟机实现中使用。 如果JIT或者调试器直接与 Java 虚拟机代码交互,可能会遇到这些操作码. 这些工具遇到保留指令,应该尝试优雅地运行, 不需要报错。
深入学习字节码与JIT编译: Virtual Call
JVM规范第3章: Chapter 3. Compiling for the Java Virtual Machine
JVM规范第7章: Java Virtual Machine Specification: Chapter 7. Opcode Mnemonics by Opcode
Byte Code Engineering Library (BCEL): https://commons.apache.org/proper/commons-bcel/
更多文章请参考GitHub上的文章翻译项目: https://github.com/cncounter/translation
同时也请各位大佬点赞Star支持!