本篇文章主要围绕字节码的指令,深入浅出的解析各种类型字节码指令,如:加载存储、算术、类型转换、对象创建与访问、方法调用与返回、控制转义、异常处理、同步等
由于字节码指令种类太多,本文作为上篇概述加载存储、算术、类型转换的字节码指令
使用idea中的插件jclasslib查看编译后的字节码指令
字节码指令集
大部分指令先以i(int)、l(long)、f(float)、d(double)、a(引用)开头
其中byte、char、short、boolean在hotspot中都是转成int去执行(使用int类型的字节码指令)
字节码指令大致分为:
- 加载与存储指令
- 算术指令
- 类型转换指令
- 对象创建与访问指令
- 方法调用与返回指令
- 操作数栈管理指令
- 控制转义指令
- 异常处理指令
- 同步控制指令
在hotspot中每个方法对应的一组字节码指令
这组字节码指令在该方法所对应的栈帧中的局部变量表和操作数栈上进行操作
字节码指令包含字节码操作指令 和 操作数 (操作数可能是在局部变量表上也可能在常量池中还可能就是常数)
加载与存储指令
加载
加载指令就是把操作数加载到操作数栈中(可以从局部变量表,常量池中加载到操作数栈)
- 局部变量表加载指令
i/l/f/d/aload
后面跟的操作数就是要去局部变量表的哪个槽取值iload_0
: 去局部变量表0号槽取出int类型值
- 常量加载指令
- 可以根据加载的常量范围分为三种(从小到大)
const < push < ldc
存储
存储指令就是将操作数栈顶元素出栈后,存储到局部变量表的某个槽中
- 存储指令
i/l/f/d/astore
后面跟的操作数就是要存到局部变量表的哪个槽istore_1
:出栈栈顶int类型的元素保存到局部变量表的1号槽
注意: 编译时就知道了局部变量表应该有多少槽的位置 和 操作数栈的最大深度(为节省空间,局部变量槽还会复用)
从常量池加载100存储到局部变量表1号槽,从常量池加载200存储到局部变量表2号槽(其中局部变量表0号槽存储this)
算术指令
算术指令将操作数栈中的俩个栈顶元素出栈作运算再将运算结果入栈
使用的是后缀表达式(逆波兰表达式),比如 3 4 + => 3 + 4
注意
- 当除数是0时会抛出ArithmeticException异常
- 浮点数转整数向0取整
- 浮点数计算精度丢失
- Infinity 计算结果无穷大
- Nan 计算结果不确定计算值
public void test1() { double d1 = 10 / 0.0; //Infinity System.out.println(d1); double d2 = 0.0 / 0.0; //NaN System.out.println(d2); //向0取整模式:浮点数转整数 //5 System.out.println((int) 5.9); //-5 System.out.println((int) -5.9); //向最接近数舍入模式:浮点数运算 //0.060000000000000005 System.out.println(0.05+0.01); //抛出ArithmeticException: / by zero异常 System.out.println(1/0); }
类型转换指令
类型转换指令可以分为宽化类型转换和窄化类型转换(对应基本类型的非强制转换和强制转换)
宽化类型转换
小范围向大范围转换
- int -> long -> float -> double
i2l
,i2f
,i2d
l2f
,l2d
f2d
byte、short、char 使用int类型的指令
注意: long转换为float或double时可能发生精度丢失
public void test2(){ long l1 = 123412345L; long l2 = 1234567891234567899L; float f1 = l1; //结果: 1.23412344E8 => 123412344 // l1 = 123412345L System.out.println(f1); double d1 = l2; //结果: 1.23456789123456794E18 => 1234567891234567940 // l2 = 1234567891234567899L System.out.println(d1); }
窄化类型转换
大范围向小范围转换
- int->byte、char、short:
i2b
,i2c
,i2s
- long->int:
l2i
- float->long、int:
f2l
,f2i
- double->float、long、int:
d2f
,d2l
,d2i
如果long,float,double要转换为byte,char,short可以先转为int再转为相对应类型
窄化类型转换会发生精度丢失
NaN和Infinity的特殊情况:
public void test3(){ double d1 = Double.NaN; double d2 = Double.POSITIVE_INFINITY; int i1 = (int) d1; int i2 = (int) d2; //0 System.out.println(i1); //true System.out.println(i2==Integer.MAX_VALUE); long l1 = (long) d1; long l2 = (long) d2; //0 System.out.println(l1); //true System.out.println(l2==Long.MAX_VALUE); float f1 = (float) d1; float f2 = (float) d2; //NaN System.out.println(f1); //Infinity System.out.println(f2); }
NaN转为整型会变成0
正无穷或负无穷转为整型会变成那个类型的最大值或最小值
对象创建与访问指令
对象创建与访问指令: 创建指令、字段访问指令、数组操作指令、类型检查指令
创建指令
new
: 创建实例
newarray
: 创建一维基本类型数组
anewarray
: 创建一维引用类型数组
multianewarray
: 创建多维数组
注意: 这里的创建可以理解为分配内存,当多维数组只分配了一维数组时使用的是anewarray
字段访问指令
getstatic
: 对静态字段进行读操作
putstatic
: 对静态字段进行写操作
getfield
: 对实例字段进行读操作
putfield
: 对实例字段进行写操作
读操作: 把要进行读操作的字段入栈
写操作: 把要写操作的值出栈再写到对应的字段
数组操作指令
b/c/s/i/l/f/d/a aload
:表示将数组中某索引元素入栈(读)
- 需要的参数从栈顶依次向下: 索引位置、数组引用
b/c/s/i/l/f/d/a astore
:表示将某值出栈并写入数组某索引元素(写)
- 需要的参数从栈顶依次向下: 要写入的值、索引位置、数组引用
注意: b开头的指令对byte和boolean通用
arraylength
: 先将数组引用出栈再将获得的数组长度入栈
类型检查指令
instanceof
: 判断某对象是否为某类的实例
checkcast
: 检查引用类型是否可以强制转换
总结
由于字节码指令种类多篇幅长,将会分为上、下篇来深入浅出解析字节码指令,本篇作为上篇深入浅出的解析字节码指令介绍、加载存储指令、算术指令、类型转换指令以及对象创建与访问指令
字节码指令大部分以i、l、f、d、a开头,分别含义对应int、long、float、double、引用,其中byte、char、short、boolean会转换为int来执行
字节码指令分为字节码操作指令和需要操作的数据,数据可能来源于局部变量表或常量池
加载指令从局部变量表或者常量池中加载数据,存储指令将存储到对应局部变量表的槽中,实例方法的局部变量表的0号槽常用来存储this,如果方法中变量是局部存在的还可能会复用槽
算术指令为各种类型和各种算术提供算术规则,在操作数栈中使用后缀表达式对操作数进行算术
类型转换分为宽化与窄化,都可能存在精度损失
对象创建与访问指令中包含创建对象,访问实例、静态字段,操作数组,类型检查等指令
最后
- 参考资料
- 《深入理解Java虚拟机》
本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~
觉得菜菜写的不错,可以点赞、关注支持哟~
有什么问题可以在评论区交流喔~