字节码指令

环境

jdk1.8

概括

  1. 助记符(mnemonic):Short description of the instruction
    对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务。例如 i 代表对int类型的数据操作
  2. JVM中的字节码指令集按照用途大致分为9类
    2.1 加载与存储指令
    2.2 算术指令
    2.3 类型转换指令
    2.4 对象的创建与访问指令
    2.5 方法调用与返回指令
    2.6 操作数栈管理指令
    2.7 比较控制指令
    2.8 异常处理指令
    2.9 同步控制指令

加载与存储指令

  1. 作用:加载与存储指令用于将数据从栈帧的局部变量表和操作数直接来回传递
  2. 常用指令
    2.1 局部变量压栈指令:将一个局部变量加载到操作数栈:xload、xload(x为i、l、f、d、a,n为0到3)
    2.2 常量入栈指令:将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst、fconst_、dconst_
    2.3 出栈装入局部变量表指令:将一个数组从操作数栈存储到局部变量表:xstore、xstore_(x为i、l、f、d、a,n为0到3);xastore(x为i、l、f、d、a、b、c、s)
    2.4 扩充局部变量表访问索引的指令:wide

局部变量压栈指令

  1. xload_(x为i、l、f、d、a,n为0~3)
  2. xload(x为i、l、f、d、a)
  3. x值为数值类型
  4. 指令xload_n表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0等指令。其中aload_n表示将个对象引用压栈
  5. 指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个比如指令iload、fload等

常量入栈指令

  1. 常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列、push系列和ldc指令
  2. 指令const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: iconst_(i从-1到5)、lconst_(1从0到1)、fconst_(f从0到2)、dconst_(d从0到1)、aconst_null
  3. 指令push系列:主要包括bipush和sipush。它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数。sipush接收16位整数,它们都将参数压入栈。
  4. 指令ldc系列:如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、 float或者String的索引,将指定的内容压入堆栈
  5. ldc_w:它接收两个8位参数,能支持的索引范围大于ldc。如果要压入的元素是long或者double类型的,则使用1dc2_w指令,使用方式都是类似的。
 public void method1() {
    int i = 3;
  }

出栈转入局部变量表指令

  1. 出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值
  2. 这类指令主要以store的形式存在,比如xstore (x为i、1、f、d、a)、 xstore_n (x 为i、1、f、d、a,n为至3)
    2.1 其中,指令istore_n将从操作数栈中弹出一个整数,并把它赋值给局部变量索引n位置
    2.2 指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置

算术指令

  1. byte、short、char和bolean类型的运算都使用int类型的指令来处理
  2. 运算模式
    2.1 向最接近数舍入模式:JVW要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的
    2.2 向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果
  3. NAN:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用MaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回 NaN
  4. 指令
    4.1 加法指令:iadd、ladd、fadd、dadd
    4.2 减法指令:isub、lsub、fsub、dsub
    4.3 乘法指令:imul、lmul、fmul、dmul
    4.4 除法指令:idiv、ldiv、fdiv、ddiv
    4.5 求余指令:irem、lrem、frem、drem
    4.6 取反指令:ineg、lneg、fneg、dneg
    4.7 自增指令:iinc
    4.8 位运算指令
    4.8.1 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
    4.8.2 按位或指令:ior、lor
    4.8.3 按位与指令:iand、land
    4.8.4 按位异或指令:ixor、lxor
  5. 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

类型转换指令

  1. 类型转换指令可以将两种不同的数值类型进行相互转换
  2. 这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题

宽化类型转换

  1. 转换规则:Java虚拟机直拟支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:
    1.1 从int类型到long、float或者double类型。对应的指令为:i21、i2f、i2d
    1.2 从1ong类型到float、double类型。对应的指令为:12f、12d
    1.3 从float类型到double类型。对应的指令为:f2d
  2. 精度损失:从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。
  3. byte等用于int类型处理
  public void method1() {
    byte b = 10;
    long l = b;
  }

窄化类型转换

  1. 转换规则:JVM支持一下窄化类型转换:
    1.1 从int类型至byte、 short或者char类型。对应的指令有: i2b、i2s、i2c从long类型到int类型。对应的指令有:12i
    1.2 从long类型到int类型。对应的指令有:12i
    1.3 从float类型到int或者long类型。对应的指令有:f2i、f21
    1.4 从double类型到int、long或者float类型。对应的指令有:d2i、d21、d2f
  2. 精度损失:窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度

对象的创建与访问指令

创建指令

创建类实例的指令

new:它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈

创建数组的指令

  1. newarray:创建基本类型数组
public void method1() {
    int[] ints = new int[10];
  }


2. anewarray:创建引用类型数组

Object[] objects = new Object[10];


3. multianewarray:创建多维数组

int[][] ints = new int[10][10];

字段访问指令

  1. 访问类字段(static字段,或者称为类变量)的指令: getstatic、putstatic
class Person {
  public static int num = 1;
}
 public void method1() {
    int num = Person.num;
  }


2. 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield、putfield

class Person {
  public static int num = 1;
  public int age = 10;
}
 public void method2() {
    int age = new Person().age;
  }

数组操作指令

数组操作指令主要有: xastore和xaload指令。具体为:

  1. 把一个数组元素加载到操作数栈的指令: baload、caload、saload、iaload、laload、faload、daload、aaload
  2. 将一个操作数栈的值存储到数组元素中的指令: bastore、 castore、 sastore、iastore、lastore、fastore、dastore、aastore

类型检查指令

  1. 检查类实例或数组类型的指令: instanceof、 checkcast
  2. 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈否则它会物出classCastExceotion异堂
  3. 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈

方法调用和返回指令

方法调用指令

  1. 方法调用指令: invokevirtual、invokeinterface、invokespecial、invokestatic . invokedynamic
  2. invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最堂见的方法分派方式
  3. invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
  4. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发
  5. invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的
  6. invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法

方法返回指令

  1. 方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的
  2. ireturn(当返回值是 boolean、byte、char、short和int类型时使用)、lreturn、freturn.dreturn和areturn
  3. return指令供声明为 void的方法、实例初始化方法以及类和接口的类初始化方法使用

操作数栈管理指令

  1. 将一个或两个元素从栈顶弹出,并且直接废弃:pop.pop2
  2. 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_×2
  3. 将栈最顶端的两个Slot数值位置交换:swap。Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令
  4. 指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等
  5. 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2. dup的系数代表要复制的slot个数
    5.1 dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据
    5.2 dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型数据
  6. 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令, dup_x1, dup2_x1,dup_x2,dup2_x2.对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置

控制转移指令

控制转移指令大体上可以分为:1:比较指令、2:条件跳转指令、3:比较条件跳转指令、4:多条件分支跳转指令、5:无条件跳转指令等

比较指令

dcmpg、dcmpl、fcmpg、fcmpl、lcmp

条件跳转指令

  1. 条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转
  2. 条件跳转指令有: ifeq, iflt, ifle,ifne,ifgt,ifge,ifnull,ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)
  3. 它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置
  4. 对于boolean、byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
  5. 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转

比较条件跳转指令

  1. 比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一
  2. 这类指令有: if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较

多条件分支跳转指令

  1. 多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch
  2. tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高
  3. 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低
  4. 对于String的比较,会把String转化为对应的hashCode值后进行排序
public void method3(String result) {
    switch (result) {
      case "tom":
        break;
      case "lucy":
        break;
    }
  }

异常处理指令

抛出异常指令

  1. athrow指令:在]ava程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现
  2. 正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上

异常处理与异常表

  1. 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的
  2. 如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:
    2.1 起始位置
    2.2 结束位置
    2.3 程序计数器记录的代码处理的偏移地址
    2.4 被捕获的异常类在常量池中的索引

同步控制指令

  1. java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的
  2. 方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法
  3. 当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置
    3.1 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁
    3.2 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁
    3.3 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放
  4. 同步一段指令集序列:通常是由java中的synchronized语句块来表示的。jvm的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义
  5. 当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为e,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块
  6. 当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态
  7. 指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的
posted @   翻蹄亮掌一皮鞋  阅读(534)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示