JVM 指令02__算术指令
一、概述
1.1、作用
算术指令用于对操作数栈栈顶的元素(如果运算只包含一个操作数,那么该元素就是栈顶的元素,如果有两个操作数,那么就是栈顶和次栈顶的元素)进行某种特定的运算,并把运算的结果重新压入操作数栈
1.2、分类
大体上来说,算术指令可以分为两大类,一类是对 整数类型 的数据进行运算的指令,另外一类是对 浮点数类型 的数据进行运算的指令
1.3、运算类型
在 Jvm 中没有直接支持 byte、short、char、boolean 类型的算术指令,对于这些数据类型统一使用 int 类型的指令来进行处理,Java 虚拟机中的实际类型和参与运算类型的对照表如下
实际的数据类型 | Jvm 指令运算时使用的数据类型 |
byte | int |
short | int |
char | int |
boolean | int |
int | int |
float | float |
double | double |
long | long |
reference | reference |
returnAddress | returnAddress |
二、指令
加法指令: iadd、fadd、dadd、ladd
减法指令: isub、fsub、dsub、lsub
乘法指令: imul、fmul、dmul、lmul (mul 是 multiply 的简写,代表乘法的意思)
除法指令: idiv、fdiv、ddiv、ldiv (div 是 divide 的简写,代表除法的意思)
求余指令: irem、frem、drem、lrem (rem 是 remainder 的简写,代表除法的意思)
取反指令: ineg、fneg、dneg、lneg(neg 是 negation 的简写,代表取反的意思)
自增指令: iinc (注意该指令只针对 int 类型的数据,其它类型的数据自增不使用 iinc 指令,使用的与常数 1 相加的操作,iinc 是在局部变量表上直接做自增的操作,并不是在操作数栈上做自增的操作)
三、案例
3.1、i++ 和 ++i 的区别
1 2 3 4 | public void method() { int i = 10 ; i++; } |
编译之后的字节码指令如下
1 2 3 4 | public void method() { int i = 10 ; ++i; } |
可以看出如果不做赋值操作的情况下 i++ 和 ++i 的字节码指令是一样的,所以我们最常见的 for 循环代码,下面两种写法是等价的
1 2 3 4 | // 写法 1 for ( int i = 0 ; i < 10 ; i++) { } // 写法 2 for ( int i = 0 ; i < 10 ; ++i) { } |
3.2、i++ 和 ++i 做赋值操作
1 2 3 4 5 | public void method() { int i = 0 ; i = i++; System.out.println(i); } |
对应的字节码文件
指令分析过程
指令 1、iconst_0: 将常数 0 压入操作数栈栈顶的位置
指令 2、istore_1: 将操作数栈栈顶的元素 0 弹出操作数栈,存入 slot1 位置
指令 3、iload_1: 将局部变量表 slot1 中的变量压入操作数栈栈顶位置
指令 4、iinc 1 by 1: 将 slot1 中存储的变量做 +1 操作
指令 5、istore_1: 将操作数栈栈顶的元素 0 弹出操作数栈,存储在局部变量表 slot1 位置
指令 6、getstatic: 获取静态字段 System.out,并将其压入操作数栈当中
指令 7、将局部变量表中 slot1 位置的元素压入操作数栈栈顶位置
指令 8、将操作数栈中的 0 和 System.out 弹出操作数栈,并调用 println() 方法进行打印输出操作,此时输出的结果为 0
然后将 i++ 换成 ++i
1 2 3 4 5 | public void method() { int i = 0 ; i = ++i; System.out.println(i); } |
对应的字节码文件
指令分析过程
指令 1、iconst_0: 将常数 0 压入操作数栈栈顶的位置
指令 2、istore_1: 将操作数栈栈顶的元素 0 弹出操作数栈,存入 slot1 位置
指令 3、iinc 1 by 1: 将局部变量表 slot1 位置的变量做 +1 操作
指令 4、iload_1: 将局部变量表中 slot1 位置的变量压入操作数栈栈顶位置
指令 5、istore_1: 将操作数栈栈顶的元素 1 弹出操作数栈,存放在局部变量表 slot1 位置
指令 6、getstatic: 获取静态字段 System.out,并将其压入操作数栈当中
指令 7、将局部变量表中 slot1 位置的元素压入操作数栈栈顶位置
指令 8、将操作数栈中的 1 和 System.out 弹出操作数栈,并调用 println() 方法进行打印输出操作,此时输出的结果为 1
从上面字节码的执行过程中可以看出 i++ 和 ++i 的区别
i++: 局部变量表中的操作数 0 先入操作数栈,然后再在局部变量表上做自增操作变成 1,最后给 i 赋值的时候操作数栈中的 0 覆盖了局部变量表中的 1,最终结果为 0 (先入操作数栈,然后在局部变量表自增 1)
++i: 局部变量表中的操作数先做自增操作变成 1,然后将 1 压入操作数栈中,最后给 i 赋值的时候操作数栈中的 1 覆盖了局部变量表中的 1,最终结果为 1 (先在局部变量表自增 1,然后再入操作数栈)
3.3、综合案例
1 2 3 4 5 | public static void method() { int i = 0 ; i = i++ + ++i; System.out.println(i); } |
上述代码编译之后反解析后的字节码指令如下
1 2 3 4 5 6 7 8 9 10 11 12 | 0 iconst_0 1 istore_1 2 iload_1 3 iinc 1 by 1 6 iinc 1 by 1 9 iload_1 10 iadd 11 istore_1 12 getstatic # 2 <java/lang/System.out : Ljava/io/PrintStream;> 15 iload_1 16 invokevirtual # 3 <java/io/PrintStream.println : (I)V> 19 return |
method 是非静态方法,所以该方法栈帧的局部变量表第一个槽位(slot0)存放的是 this 指针的内存地址值,除了 this 指针外整个方法只有一个 int 类型的局部变量 i,故局部变量表的最大槽位数为 2,方法中只有加法操作,所以操作数栈的最大深度为 2
指令 1、iconst_0: 将 int 类型的常数 0 压入操作数栈
指令 2、istore_1: 将操作数栈的栈顶元素(0)弹出操作数栈,然后存放在局部变量表索引下标为 1 的位置(slot1 槽位)
指令 3、iload_1: 将局部变量表 slot1 的变量值压入操作数栈的栈顶位置
指令 4、iinc 1 by 1: 将局部变量表下标为 1 的变量做加 1 操作
指令 5、iinc 1 by 1: 将局部变量表下标为 1 的变量做加 1 操作
指令 6、iload_1: 将局部变量表下标为 1 的槽位中的变量压入操作数栈的栈顶位置,原先已经在栈位置的常数 0 就会被压入到次栈顶的位置
指令 7、iadd: 将位于栈顶的操作数 2 和位于次栈顶的操作数 0 弹出操作数栈,执行加法计算,计算完成之后将结果重新存回操作数栈的栈顶位置
指令 8、istore_1: 将位于操作数栈栈顶位置的元素 2 弹出操作数栈,存入局部变量表 slot1 位置
指令 9、getstatic: 获取静态字段 System.out,并将其压入操作数栈当中
指令 10、iload_1: 将局部变量表 slot1 中的变量压入操作数栈当中
指令 11、invokevirtual: 将操作数栈中的 System.out 和 2 弹出操作数栈,调用 println() 方法进行打印
综上分析,上述代码执行之后 i 的值为 2
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2020-11-27 Linux 使用 yum install 安装程序时,提示另外一个程序锁定了 yum 等待它退出...
2020-11-27 Centos 6.8 安装 Docker 及 Docker 入门介绍