深入浅出JVM(九)之字节码指令(上篇)

简介: 深入浅出JVM(九)之字节码指令(上篇)

本篇文章主要围绕字节码的指令,深入浅出的解析各种类型字节码指令,如:加载存储、算术、类型转换、对象创建与访问、方法调用与返回、控制转义、异常处理、同步等

由于字节码指令种类太多,本文作为上篇概述加载存储、算术、类型转换的字节码指令

使用idea中的插件jclasslib查看编译后的字节码指令

字节码指令集

大部分指令先以i(int)、l(long)、f(float)、d(double)、a(引用)开头

其中byte、char、short、boolean在hotspot中都是转成int去执行(使用int类型的字节码指令)

字节码指令大致分为:

  1. 加载与存储指令
  2. 算术指令
  3. 类型转换指令
  4. 对象创建与访问指令
  5. 方法调用与返回指令
  6. 操作数栈管理指令
  7. 控制转义指令
  8. 异常处理指令
  9. 同步控制指令

在hotspot中每个方法对应的一组字节码指令

这组字节码指令在该方法所对应的栈帧中的局部变量表和操作数栈上进行操作

字节码指令包含字节码操作指令 和 操作数 (操作数可能是在局部变量表上也可能在常量池中还可能就是常数)

加载与存储指令

加载

加载指令就是把操作数加载到操作数栈中(可以从局部变量表,常量池中加载到操作数栈)

  • 局部变量表加载指令
  • i/l/f/d/aload 后面跟的操作数就是要去局部变量表的哪个槽取值
  • iload_0: 去局部变量表0号槽取出int类型值
  • 常量加载指令
  • 可以根据加载的常量范围分为三种(从小到大) const < push < ldc

存储

存储指令就是将操作数栈顶元素出栈后,存储到局部变量表的某个槽中

  • 存储指令
  • i/l/f/d/astore 后面跟的操作数就是要存到局部变量表的哪个槽
  • istore_1:出栈栈顶int类型的元素保存到局部变量表的1号槽

注意: 编译时就知道了局部变量表应该有多少槽的位置 和 操作数栈的最大深度(为节省空间,局部变量槽还会复用)

image.png

从常量池加载100存储到局部变量表1号槽,从常量池加载200存储到局部变量表2号槽(其中局部变量表0号槽存储this)

算术指令

算术指令将操作数栈中的俩个栈顶元素出栈作运算再将运算结果入栈

使用的是后缀表达式(逆波兰表达式),比如 3 4 + => 3 + 4

注意

  1. 当除数是0时会抛出ArithmeticException异常
  2. 浮点数转整数向0取整
  3. 浮点数计算精度丢失
  4. Infinity 计算结果无穷大
  5. 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
  • i2li2fi2d
  • l2fl2d
  • 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: i2bi2ci2s
  • long->int: l2i
  • float->long、int: f2lf2i
  • double->float、long、int: d2fd2ld2i

如果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

image.png

字段访问指令

getstatic: 对静态字段进行读操作

putstatic: 对静态字段进行写操作

getfield: 对实例字段进行读操作

putfield: 对实例字段进行写操作

读操作: 把要进行读操作的字段入栈

写操作: 把要写操作的值出栈再写到对应的字段

image.png

数组操作指令

  • b/c/s/i/l/f/d/a aload:表示将数组中某索引元素入栈(读)
  • 需要的参数从栈顶依次向下: 索引位置、数组引用
  • b/c/s/i/l/f/d/a astore:表示将某值出栈并写入数组某索引元素(写)
  • 需要的参数从栈顶依次向下: 要写入的值、索引位置、数组引用

image.png

注意: b开头的指令对byte和boolean通用

  • arraylength: 先将数组引用出栈再将获得的数组长度入栈

image.png

类型检查指令

instanceof: 判断某对象是否为某类的实例

checkcast: 检查引用类型是否可以强制转换

image.png

总结

由于字节码指令种类多篇幅长,将会分为上、下篇来深入浅出解析字节码指令,本篇作为上篇深入浅出的解析字节码指令介绍、加载存储指令、算术指令、类型转换指令以及对象创建与访问指令

字节码指令大部分以i、l、f、d、a开头,分别含义对应int、long、float、double、引用,其中byte、char、short、boolean会转换为int来执行

字节码指令分为字节码操作指令和需要操作的数据,数据可能来源于局部变量表或常量池

加载指令从局部变量表或者常量池中加载数据,存储指令将存储到对应局部变量表的槽中,实例方法的局部变量表的0号槽常用来存储this,如果方法中变量是局部存在的还可能会复用槽

算术指令为各种类型和各种算术提供算术规则,在操作数栈中使用后缀表达式对操作数进行算术

类型转换分为宽化与窄化,都可能存在精度损失

对象创建与访问指令中包含创建对象,访问实例、静态字段,操作数组,类型检查等指令

最后

  • 参考资料
  • 《深入理解Java虚拟机》

本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~

觉得菜菜写的不错,可以点赞、关注支持哟~

有什么问题可以在评论区交流喔~


相关文章
|
6月前
|
安全 Java
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
对 JVM 的类加载机制以及寻找字节码文件的“双亲委派模型”的理解
38 0
|
16天前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
19 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
17天前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
18 3
|
2月前
|
安全 前端开发 Java
浅析JVM invokedynamic指令与Java Lambda语法的深度融合
在Java的演进历程中,Lambda表达式无疑是Java 8引入的一项革命性特性,它极大地简化了函数式编程在Java中的应用,使得代码更加简洁、易于阅读和维护。而这一切的背后,JVM的invokedynamic指令功不可没。本文将深入探讨invokedynamic指令的工作原理及其与Java Lambda语法的紧密联系,带您领略这一技术背后的奥秘。
27 1
|
3月前
|
Java
Java常见JVM虚拟机指令(47个)
Java常见JVM虚拟机指令(47个)
59 3
Java常见JVM虚拟机指令(47个)
|
3月前
|
缓存 前端开发 Java
浅析JVM invokedynamic指令与Java Lambda语法
【8月更文挑战第27天】在Java的演进历程中,invokedynamic指令的引入和Lambda表达式的出现无疑是两大重要里程碑。它们不仅深刻改变了Java的开发模式和性能表现,还极大地推动了Java在函数式编程和动态语言支持方面的进步。本文将从技术角度浅析JVM中的invokedynamic指令及其与Java Lambda语法的紧密联系。
50 0
|
4月前
|
监控 Java Linux
Linux下JVM相关指令详解及案例介绍
Linux下JVM相关指令详解及案例介绍
51 1
|
5月前
|
存储 Java 编译器
JVM系列7-虚拟机字节码执行引擎
JVM系列7-虚拟机字节码执行引擎
28 1
|
4月前
|
存储 运维 Java
Java中的字节码与JVM指令集详解
Java中的字节码与JVM指令集详解
|
6月前
|
Java 索引
【JVM】字节码文件的组成部分
【JVM】字节码文件的组成部分
52 1