相关实例均使用 Oracle JDK 1.8 编译, 并使用 javap 生成字节码指令清单.
算术运算
Java 虚拟机通常基于操作数栈进行算术运算. 只有 iinc 指令例外, 它直接对局部变量进行自增操作.
实例代码
- int align2agrain(int i, int grain) {
- return ((i + grain - 1) & ~(grain - 1));
- }
字节码指令序列
以上指令, 并没有出现取反的指令操作. 因为 JVM 并没有提供取反指令, 而是使用异或指令来实现取反.
对一个数进行取反, 相当于该数的二进制每一位与 1 进行异或操作. 由于 - 1 的补码二进制表示为全部都是 1, 因此对一个数进行取反, 也相当于 - 1 与该数进行异或.
-1 的原码, 反码和补码表示
[10000001]原 =[11111110]反 =[11111111]补
异或实现取反
~x = -1^x
访问运行时常量池
实例代码
- void useManyNumeric() {
- int i = 100;
- int j = 1000000;
- long l1 = 1;
- long l2 = 0xffffffff;
- double d = 2.2;
- }
字节码指令序列
ldc,ldc_w: 将 int,float 或 String 类型常量值从常量池中推送至栈顶.
ldc2_w: 将 long,double 类型常量值从常量池中推送至栈顶.(只有宽索引版本)
其中, ldc_w 和 ldc2_w 属于宽索引指令, 即指令对应的 (索引值) 参数为 2 个字节. 而 ldc 指令对应的 (索引值) 参数为 1 个字节.
当运行时常量池中的常量个数超过 256 个 (1 个字节所能代表的数量) 时, 需要使用支持 2 个字节索引值的指令 ldc_w 指令来代替 ldc 访问常量池.
在局部变量表中, long 和 double 类型的数据占用两个连续的局部变量, 并且采用两个局部变量中较小的索引值来定位其数据. 因此, lstore_3,lstore 5,dstore 7 这三个指令实际存入的局部变量索引号分别为 3 和 4,5 和 6,7 和 8.(局部变量表的索引值从 0 开始)
控制结构
Java 虚拟机会根据数据类型的变化来生成不同的条件跳转语句.
while 实例 1
- void whileInt() {
- int i = 0;
- while (i <100) {
- i++;
- }
- }
字节码指令序列
iinc 用于实现局部变量的自增操作. 在所有字节码指令中, 只有该指令可直接用于操作局部变量.
对于循环的实现, 将条件判断放在循环的最前面不是更易于理解, 为什么要放在最后面? 让我们来看看放在最前面的指令序列:
- 2 iload_1
- 3 bipush 100
- 5 if_icmpge 14
- 8 iinc 1,1
- 11 goto 2
显然, 两种实现方式第 1 次循环都要执行 5 条执行. 但对于后续的循环, 前者只需要执行 4 条指令, 而后者则需要执行 5 条指令. 因此, 将条件判断放在循环的最后面可以更高效的执行循环.
while 实例 2
- void whileDouble() {
- double i = 0;
- while (i < 100.1) {
- i++;
- }
- }
字节码指令序列
由于 iinc 只针对 int 类型的局部变量进行自增操作, JVM 并没有提供相应的指令来操作 double 类型. 因此, 需要借助 dadd 来实现 double 类型的自增操作.
同样, 对于数值类型, 以 if 开头的比较跳转指令, 都只支持 int 类型 (对于非数值类型, if 比较跳转指令还支持引用类型数值). 因此, JVM 另外提供了 dcmpg,dcmpl 来比较两个 double 类型数值的大小, 然后将比较结果(1,0,-1) 压入栈顶. 最后, 再使用 int 类型的 if 判断指令来进行判断跳转.
dcmpg 与 dcmpl 的区别仅在于, 当比较的其中一个值为 NaN 时, dcmpg 将 1 压入栈顶, 而 dcmpl 将 - 1 压入栈顶.
ldc 相关指令都是将常量值从常量池中推至栈顶, 前面 "访问运行时常量池" 一节已经介绍过了.
对于 for 循环分析, 请看第一篇: JVM 指令分析实例一(常量, 局部变量, for 循环) https://mp.weixin.qq.com/s/anzDcV01y13pdFP7U42G-w
if 实例 1
- int lessThan100(double d) {
- if (d < 100.0) {
- return 1;
- } else {
- return -1;
- }
- }
字节码指令序列
if 实例 2
- int greaterThan100(double d) {
- if (d> 100.0) {
- return 1;
- } else {
- return -1;
- }
- }
字节码指令序列
if 实例 2 与 if 实例 1 的差别仅在于比较符号由小于号改为大于号, 因此 ifge 指令也相应的变成 ifle 指令.
如果细心一点, 还会发现一个差异, double 比较指令由 dcmpg 变成了 dcmpl.
那么, JVM 在什么情况下使用 dcmpg, 什么情况下又会使用 dcmpl 呢? 为了理解这一点, 我们需要先回顾一下浮点数中的 NaN 值.
Java 虚拟机关于浮点数的规范
浮点类型包含 float 和 double 类型两种, 32 位单精度和 64 位双精度与 IEEE 754 格式的取值与操作是一致的.
NaN 值用于表示某此无效的运算操作, 例如 0 除以 0 等情况.
只要有操作数是 NaN, 那么对它进行任何数值比较和等值测试都会返回 false. 任何数值与 NaN 进行不等值比较都会返回 true.
有了以上知识, 我们再回到例子来分析一下.
我们知道, dcmpg 与 dcmpl 的作用都是比较两个 double 类型数值的大小, 并将结果 (1,0,-1) 压入栈顶. 区别仅在于, 当比较的其中一个值为 NaN 时, dcmpg 将 1 压入栈顶, 而 dcmpl 将 - 1 压入栈顶.
对于 if (d < 100.0) {}, 隐含了两个条件, 一个是 d 必须小于 100.0, 另一个是 d 不能为 NaN(如果为 NaN 会返回 false). 因此, NaN 属于该条件之外的情况.
当 if (d < 100.0) {} 成立时, 执行比较指令之后结果为 - 1. 由于满足该条件时 d 不能为 NaN, 显然当 d 为 NaN 时比较结果不能为 - 1. 因此比较指令排除 dcmpl, 只能使用 dcmpg 指令.
维基百科对 NaN 的定义
NaN(Not a Number, 非数)是计算机科学中数值数据类型的一类值, 表示未定义或不可表示的值. 常在浮点数运算中使用. 首次引入 NaN 的是 1985 年的 IEEE 754 浮点数标准.
返回 NaN 的运算有如下三种:
来源: https://juejin.im/post/5bb259d3f265da0a906f81b2