在 Java 虚拟机指令(操作码)集 中给出了一个操作码的列表。针对所有的指令,仅仅给出了一个大概介绍,对理解来说可以说毫无助力。为了弥补这个短板,这里也学习 “Hessian 协议解释与实战”系列 那样,来一个详细解释和实战,配合实例来做个深入分析和讲解。这是这个系列的第一篇文章,就以列表中第一部分“常量”指令开始。
从 Java 虚拟机指令(操作码)集 列表上来看,一共 21 个指令;按照处理数据的类型,合并同类项后,剩下有 nop 、、、、、、、、和等几个指令。下面,按照顺序,对其进行一一讲解。
操作码助记符的首字母一般是有特殊含义的,表示操作码所作用的数据类型: i 代表对 int 类型的数据操作; l 代表 long ; s 代表 short ; b 代表 byte ; c 代表 char ; f 代表 float , d 代表 double ; a 代表 reference。 |
根据 Chapter 6. The Java Virtual Machine Instruction Set:nop 来看,就是“Do nothing”,暂时没有找到使用方法。就不做多介绍,后续看到相关资料,再做补充。
*const 是一个大类,根据不同的操作数类型,又分为、、、和等几个分类。
const 指令主要就是将相关类型的“常量”(与 Java 使用 static final 修饰的“常量”的定义不同,这里是 Java 代码中存在的“直接量”,比如给对象赋值的 `null`等)推送至栈顶。下面对其一一介绍。
这里只有 aconst_null ,直接上代码演示:
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 aconst_null 示例
*/
public Object testAconst() {
return null;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public java.lang.Object testAconst();
Code:
0: aconst_null
1: areturn
}
在上述结果中,我们如愿看到了 aconst_null 操作码。从上面的 testAconst 方法的指令来看,是将 null 加载到栈顶,然后返回。与我们的代码是一致的。
对比了 Java 8 与 Java 17 的编译结果。从 javap -c 的输出上来看,两者没有差异。以后不再赘述。如有问题,再支出。 |
iconst 的完整写法是 iconst_ , 包含 iconst_m1 、 iconst_0 、 iconst_1 、 iconst_2 、 iconst_3 、 iconst_4 和 iconst_5 五个操作码。
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 iconst_ 示例
*/
public void testIconst() {
int im1 = -1;
int i0 = 0;
int i1 = 1;
int i2 = 2;
int i3 = 3;
int i4 = 4;
int i5 = 5;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public void testIconst();
Code:
0: iconst_m1
1: istore_1
2: iconst_0
3: istore_2
4: iconst_1
5: istore_3
6: iconst_2
7: istore 4
9: iconst_3
10: istore 5
12: iconst_4
13: istore 6
15: iconst_5
16: istore 7
18: return
}
在上述结果中,依次看到了 iconst_m1 、 iconst_0 、 iconst_1 、 iconst_2 、 iconst_3 、 iconst_4 和 iconst_5 操作码。从上面的 testIconst 方法的指令来看,是依次将 int 的 -1 、 0 、 1 、 2 、 3 、 4 和 5`加载到栈顶并栈顶数据赋值给第二、三、四、五、六和七个(下标从 `0 开始)变量。与我们的代码是一致的。
lconst 的完整写法是 lconst_
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 lconst_ 示例
*/
public void testLconst() {
long l0 = 0L;
long l1 = 1L;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public void testLconst();
Code:
0: lconst_0
1: lstore_1
2: lconst_1
3: lstore_3
4: return
}
在上述结果中,依次看到了 lconst_0 和 lconst_1 操作码。从上面的 testLconst 方法的指令来看,是依次将 long 的 0 和 1`加载到栈顶并栈顶数据赋值给第二和四个(下标从 `0 开始)变量。与我们的代码是一致的。
细心的朋友可能发现了 lstore_1 之后,直接就是 lstore_3 ,为什么会有一个间隙呢? 这是因为 long 类型的数据在本地变量表中占据两个槽位,并且使用低槽位来表示该数字。所以,就会跳过一个槽位。 下面将要介绍的 dconst 也会有类似问题,就不再重复解释了。 |
fconst 的完整写法是 fconst_
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 fconst_ 示例
*/
public float testFconst() {
// 依次替换为 1.0F 和 2.0F,编译查看结果
return 0.0F;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public float testFconst();
Code:
0: fconst_0
1: freturn
}
在上述结果中,就看到了 fconst_0 。从上面的 testFconst 方法的指令来看,是依次将 float 的 0.0 到栈顶。与我们的代码是一致的。
将上述代码中的 0.0F 依次替换为 1.0F 和 2.0F ,编译查看结果,也会看到 fconst_1 和 fconst_2 。
dconst 的完整写法是 dconst_
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 dconst_ 示例
*/
public double testDconst() {
// 替换为 1.0,编译查看结果
return 0.0;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public double testDconst();
Code:
0: dconst_0
1: dreturn
}
在上述结果中,就看到了 dconst_0 。从上面的 testDconst 方法的指令来看,是将 dconst_0 的 0.0 到栈顶。与我们的代码是一致的。
将上述代码中的 0.0 替换为 1.0 ,编译查看结果,也会看到 dconst_1 。
bipush 只有一个操作码 bipush ,后面紧跟一个字节的数据。作用是将后面一个字节的数据推到栈顶。
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 bipush 示例
*/
public int testBipush() {
// 替换为 -128 ~ -2 和 6 ~ 127 之间的整数,
// 编译查看结果
return 6;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int testBipush();
Code:
0: bipush 6
2: ireturn
}
在上述结果中,就看到了 bipush 。从上面的 testBipush 方法的指令来看,是将后面参数 6 到栈顶。来看一下原始数据。使用合适的编辑器,打开 Example.class 文件,调整成二进制(或者十六进制)模式,如下图所示:
可以在 Java 虚拟机指令(操作码)集 中,查找 bipush 和 ireturn 对应的编码是 0x10 和 0xAC ,中间有一个 6 (编码为 0x06 ),符合上述要求的字节序列,已经在上图中标注出来。如果把 6 改为 127 ,那么显示就如下图:
将上述代码中的 6 替换为 -128 ~ -2 和 6 ~ 127 的整数,编译查看结果,也都会看到 bipush 。之所以是这个数字区间,也是因为后面就处理一个字节的数据,一个字节内能存放的数字也就是这么大区间啦。
结合前面介绍的来看,处理思路和 Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 的处理思路是一样的,尽可能减少字节,提高处理效率。
sipush 只有一个操作码 sipush ,后面紧跟两个字节的数据。作用是将后面两个字节的数据推到栈顶。
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 sipush 示例
*/
public int testSipush() {
// 替换为 -32768 ~ -129 和 128 ~ 32767 之间的整数,
// 编译查看结果
return 128;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
$ javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public int testSipush();
Code:
0: sipush 128
3: ireturn
}
在上述结果中,就看到了 sipush 。从上面的 testSipush 方法的指令来看,是将后面参数 128 到栈顶。来看一下原始数据。使用合适的编辑器,打开 Example.class 文件,调整成二进制(或者十六进制)模式,如下图所示:
将上述代码中的 128 替换为 -32768 ~ -129 和 128 ~ 32767 之间的整数,编译查看结果,也都会看到 sipush 。
ldc 有两种形式和,下面进行分别介绍。
ldc 只有一个操作码 ldc ,后面紧跟的是常量池的索引。作用是将索引指向的常量池中的数据推到栈顶。数据类型可以是: int 、 float 或 String 。
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 ldc 示例
*/
public int testLdc() {
// 替换为除上述内容提到的 int 和 float 之外的值,或者字符串
// 编译查看结果
return 32768;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
# 由于需要查看常量池中的内容,由 javap -c 替换为 javap -v
$ javap -v Example
Classfile /Users/lijun695/Documents/byte-buddy-tutorial/src/main/java/Example.class
Last modified Sep 3, 2022; size 250 bytes
MD5 checksum 5776ccc3c6e038fbe0f77473cd7a42fc
Compiled from "Example.java"
public class Example
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."":()V
#2 = Integer 32768
#3 = Class #14 // Example
#4 = Class #15 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 testLdc
#10 = Utf8 ()I
#11 = Utf8 SourceFile
#12 = Utf8 Example.java
#13 = NameAndType #5:#6 // "":()V
#14 = Utf8 Example
#15 = Utf8 java/lang/Object
{
public Example();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
public int testLdc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // int 32768
2: ireturn
LineNumberTable:
line 13: 0
}
SourceFile: "Example.java"
在上述结果中,就看到了 ldc 。从上面的 testLdc 方法的指令来看, ldc 是将后面参数 #2 指向的上面的 Constant pool 中的第二个数据 32768 到栈顶。
将上面代码中的 32768 替换为除上述内容提到的 int 和 float 之外的值,或者字符串,也可以查看到相同的结果。
关于字符串在 Constant pool 的处理过程略复杂,这里不再详细介绍。再专门行文介绍。 |
对比了 Java 8 与 Java 17 的编译结果,从 javap -v 的结果来看,差异还是蛮大的,目前主要观察到两点:
至于变化原因,后续再探究。 |
暂时没有找到合适的示例。后续找到再来补充。
ldc 只有一个操作码 ldc ,后面紧跟的是常量池的索引。作用是将索引指向的常量池中的数据推到栈顶。数据类型可以是: int 、 float 或 String 。
/
* 字节码示例代码
*
* @author D瓜哥 · https://www.diguage.com
*/
public class Example {
/
* 操作码 ldc2_w 示例
*/
public long testLdc2_w() {
// 替换为 long 和 double 类型除上述内容提到的之外的值,
// 编译查看结果
return 2L;
}
}
使用 javac Example.java 编译,然后使用 javap 来查看编译的结果:
# 由于需要查看常量池中的内容,由 javap -c 替换为 javap -v
$ javap -v Example
Classfile /Users/lijun695/Documents/byte-buddy-tutorial/src/main/java/Example.class
Last modified Sep 3, 2022; size 258 bytes
MD5 checksum e81e3682cef33eeb28eceed93df1e938
Compiled from "Example.java"
public class Example
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."":()V
#2 = Long 2l
#4 = Class #15 // Example
#5 = Class #16 // java/lang/Object
#6 = Utf8
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 testLdc2_w
#11 = Utf8 ()J
#12 = Utf8 SourceFile
#13 = Utf8 Example.java
#14 = NameAndType #6:#7 // "":()V
#15 = Utf8 Example
#16 = Utf8 java/lang/Object
{
public Example();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
public long testLdc2_w();
descriptor: ()J
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // long 2l
3: lreturn
LineNumberTable:
line 14: 0
}
SourceFile: "Example.java"
在上述结果中,就看到了 ldc2_w 。从上面的 testLdc2_w 方法的指令来看, ldc2_w 是将后面参数 #2 指向的上面的 Constant pool 中的第二个数据 2l 到栈顶。
将上面代码中的 2L 替换为 long 和 double 类型除上述内容提到的之外的值,也可以查看到相同的结果。
最后使用一张图来总结一下 int 的加载:
从这张图与 Hessian 协议解释与实战(一):布尔、日期、浮点数与整数 中关于 int 编码的图做对比来看,更容易理解对 int 的优化。
留言与评论(共有 0 条评论) “” |