大家好,我是二哥呀。
Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,“Java 字节码难学吗?我能不能学会啊?”
讲良心话,不是我谦虚,一开始学 Java 字节码和 Java 虚拟机方面的知识我也感觉头大!但硬着头皮学了一阵子之后,突然就开窍了,觉得好有意思,尤其是明白了 Java 代码在底层竟然是这样执行的时候,感觉既膨胀又飘飘然,浑身上下散发着自信的光芒!
我在 CSDN 共输出了 100 多篇 Java 方面的文章,总字数超过 30 万字, 内容风趣幽默、通俗易懂,收获了很多初学者的认可和支持,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心内容。
为了帮助更多的 Java 初学者,我“一怒之下”就把这些文章重新整理并开源到了 GitHub,起名《教妹学 Java》,听起来是不是就很有趣?
GitHub 开源地址(欢迎 star):https://github.com/itwanger/jmx-java
Java 官方的虚拟机 Hotspot 是基于栈的,而不是基于寄存器的。
基于栈的优点是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所需要的指令数也比寄存器的要多,需要频繁的入栈和出栈。
基于寄存器的优点是速度快,有利于程序运行速度的优化,但操作数需要显式指定,指令也比较长。
Java 字节码由操作码和操作数组成。
操作码(Opcode):一个字节长度(0-255,意味着指令集的操作码总数不可能超过 256 条),代表着某种特定的操作含义。
操作数(Operands):零个或者多个,紧跟在操作码之后,代表此操作需要的参数。
由于 Java 虚拟机是基于栈而不是寄存器的结构,所以大多数指令都只有一个操作码。比如 aload_0(将局部变量表中下标为 0 的数据压入操作数栈中)就只有操作码没有操作数,而 invokespecial #1(调用成员方法或者构造方法,并传递常量池中下标为 1 的常量)就是由操作码和操作数组成的。
01、加载与存储指令
加载(load)和存储(store)相关的指令是使用最频繁的指令,用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
1)将局部变量表中的变量压入操作数栈中
xload_(x 为 i、l、f、d、a,n 默认为 0 到 3),表示将第 n 个局部变量压入操作数栈中。
xload(x 为 i、l、f、d、a),通过指定参数的形式,将局部变量压入操作数栈中,当使用这个指令时,表示局部变量的数量可能超过了 4 个
解释一下。
x 为操作码助记符,表明是哪一种数据类型。见下表所示。
像 arraylength 指令,没有操作码助记符,它没有代表数据类型的特殊字符,但操作数只能是一个数组类型的对象。
大部分的指令都不支持 byte、short 和 char,甚至没有任何指令支持 boolean 类型。编译器会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为 int 类型,将 boolean 和 char 零位扩展(Zero-Extend)为 int 类型。
举例来说。
private void load(int age, String name, long birthday, boolean sex) {
System.out.println(age + name + birthday + sex);
}
通过 jclasslib 看一下 load() 方法(4 个参数)的字节码指令。
iload_1:将局部变量表中下标为 1 的 int 变量压入操作数栈中。
aload_2:将局部变量表中下标为 2 的引用数据类型变量(此时为 String)压入操作数栈中。
lload_3:将局部变量表中下标为 3 的 long 型变量压入操作数栈中。
iload 5:将局部变量表中下标为 5 的 int 变量(实际为 boolean)压入操作数栈中。
通过查看局部变量表就能关联上了。
2)将常量池中的常量压入操作数栈中
根据数据类型和入栈内容的不同,此类又可以细分为 const 系列、push 系列和 Idc 指令。
const 系列,用于特殊的常量入栈,要入栈的常量隐含在指令本身。
push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数作为参数,后者接收 16 位整数。
Idc 指令,当 const 和 push 不能满足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。
Idc_w:接收两个 8 位数,索引范围更大。
如果参数是 long 或者 double,使用 Idc2_w 指令。
举例来说。
public void pushConstLdc() { // 范围 [-1,5] int iconst = -1; // 范围 [-128,127] int bipush = 127; // 范围 [-32768,32767] int sipush= 32767; // 其他 int int ldc = 32768; String aconst = null; String IdcString = "沉默王二"; }
通过 jclasslib 看一下 pushConstLdc() 方法的字节码指令。
iconst_m1:将 -1 入栈。范围 [-1,5]。
bipush 127:将 127 入栈。范围 [-128,127]。
sipush 32767:将 32767 入栈。范围 [-32768,32767]。
ldc #6 <32768>:将常量池中下标为 6 的常量 32768 入栈。
aconst_null:将 null 入栈。
ldc #7 <沉默王二>:将常量池中下标为 7 的常量“沉默王二”入栈。
3)将栈顶的数据出栈并装入局部变量表中
主要是用来给局部变量赋值,这类指令主要以 store 的形式存在。
xstore_(x 为 i、l、f、d、a,n 默认为 0 到 3)
xstore(x 为 i、l、f、d、a)
明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就会轻松得多,作用反了一下而已。
大家来想一个问题,为什么要有 xstore_ 和 xload_ 呢?它们的作用和 xstore n、xload n 不是一样的吗?
xstore_ 和 xstore n 的区别在于,前者相当于只有操作码,占用 1 个字节;后者相当于由操作码和操作数组成,操作码占 1 个字节,操作数占 2 个字节,一共占 3 个字节。
由于局部变量表中前几个位置总是非常常用,虽然 xstore_<n> 和 xload_<n> 增加了指令数量,但字节码的体积变小了!