1、概述

  • 属于基本的执行指令
  • 由操作码(1个字节)和操作数组成

1.1、执行模型

  • 执行模型如下伪代码

1.2、字节码和数据类型

  • 大多数指令都包含了操作所对应的数据类型信息,比如iload从局部变量表中加载int数据到操作数栈
  • 各个数据类型简写

  • 有的指令看不出操作数类型,如arraylength指令,但是它只能操作数组类型对象
  • 有的指令与操作数无关,如goto
  • byte、char、short、boolean等类型,并没有指令去支持,编译在运行时,以四个字节为一个单位,所以会将以上数据类型拓展为int类型(byte和char的数据符号位拓展为int,boolean和char的数据零位拓展为int),在运行时用int的指令去处理。
  1. 符号位拓展:当用更多的内存存储某一个有符号数时,由于符号位位于该数的第一位,扩展之后,符号位仍然需要位于第一位,所以,当扩展一个负数的时候需要将扩展的高位全赋为1;对于正数而言,符号扩展和零扩展是一样的,因为符号位就是0。
  2. 零位拓展:不论其符号位是多少,高位全都补0。

1.3、指令分类

  • 大致分为9类:加载与存储;算数;类型转换;对象的创建与访问;方法的调用与返回、操作数栈管理、比较控制、异常处理、同步控制等
  • 指令可以从局部变量表、常量池、堆中对象、方法调用、系统调用中取得数据(值或引用都可以),然后压入操作数栈
  • 可以从操作数栈中取出一个或多个值,完成加减乘除或其他操作

2、加载与存储指令

  • 将数据在栈帧的局部变量表和操作数栈中来回传递
  • 有的指令,虽然没有操作数,但是会将操作数隐含在操作码中,比如iload_0将局部变量表中索引为0的数据压入操作数栈中

2.1、局部变量压栈指令

  • 将局部变量表中的数据压入栈中
  • 指令大体分为两类:
  1. xload_<n>,x可以是i、l、f、d、a,n是0到3,代表局部变量表索引
  2. xload,x可以是i、l、f、d、a

2.2、常量入栈指令

  • 将常数压入操作数栈,根据数据类型和入栈内容不同,分为constpushldc系列
  • const系列指令:用于特定的常量(具体的数字)入栈,操作数包含在操作码中
  1. iconst_<i>:i的范围是-1到5,表示一个实际的数,如iconst_m1压入-1,iconst_4压入4
  2. lconst_<l>:l的范围是0到1
  3. fconst_<f>:f的范围是0到2
  4. dconst_<d>:d的范围是0到1
  5. aconst_null:引用类型,默认为null,压入null
  • push指令系列
  1. bipush:接收8位整数入栈(-128~127)
  2. sipush:接收16位整数入栈(-32768~32767)
  • ldc指令系列:万能指令,以上两种都不行的,由idc实现
  1. ldc接收8位参数,指向常量池中的int、float或String,将常量池中指定内容压入栈(根据索引搜索)
  2. ldc_w接收16位参数,索引常量池的范围更大
  3. ldc2_w用于压入long类型和double类型数据

2.3、出栈装入局部变量表指令

  • 将操作数栈中栈顶元素弹出,装入局部变量表中指定的位置
  • 指令以store形式存在,分为xstore和xstore_n
  1. xstore:x为i、l、f、d、a,在后面需要跟一个byte的参数指定存带局部变量表中哪个位置
  2. xstore_n:x为i、l、f、d、a,n代表局部变量表中的索引,即将栈顶元素存到局部变量表中哪个位置

3、算数指令

  • 对两个操作数栈上的值进行运算,并发结果压入操作数栈
  • 大体上分为两类:整数运算和浮点运算
  • 没有专门针对byte、short、char、boolean的算数指令,对于这四个类型,使用int类型的指令来处理

  • 算数溢出,指运算结果超过类型能表示的最大值,比如两个很大的正整数相加,得到一个负数,Java虚拟机对此没有规定,仅规定除法时除数为0的话有ArithmeticException异常
  • 运算模式
  1. 向最接近数舍入模式,类似四舍五入
  2. 向零舍入模式,类似向下取整
  • NaN,当操作溢出时,使用无穷大表示,但是如果运算结果没有数学定义,那么使用NaN表示

3.1、算数运算符

  • 加法指令:iadd、ladd、fadd、dadd
  • 减法指令:isub、lsub、fsub、dsub
  • 乘法指令:imul、lmul、fmul、dmul
  • 除法指令:idiv、ldiv、fdiv、ddiv
  • 求余指令:irem、lrem、frem、drem
  • 取反指令:ineg、lneg、fneg、dneg
  • 自增指令:iinc(在局部变量表内执行)
  • 位运算指令,分为:
  1. 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
  2. 按位或指令:ior、lor
  3. 按位与指令:iand、land
  4. 按位异或指令:ixor、lxor
  • 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

3.2、比较指令

  • 比较栈顶两个元素的大小,并将比较结果入栈
  • 有dcmpg、dcmpl、fcmpg、fcmpl、lcmp,首字母d表示double,f表示float,l表示long
  • double和float类型,因为有NaN的存在,所以有两个版本(g和l结尾)。dcmpg(fcmpg)遇到NaN会压入1,dcmpl(fcmpl)遇到NaN会压入-1
  • 栈顶v2,次栈顶v1,如果v1=v2,压入0;v1>v2,压入1;v1<v2,压入-1

4、类型转换指令

  • 将不同的数值类型相互转换
  • 用于用户代码中的显式类型转换,或者处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题(比如short没有对应的字节码指令)

4.1、宽化类型转换

4.1.1、转换规则

  • 小范围向大范围类型安全转换,不需要指令执行,如:
  1. int到long、float、double,对应指令为i2l、i2f、i2d
  2. long到float、double,对应指令为l2f、l2d
  3. float到double,对应指令为f2d
  4. 指令在操作数栈内执行
  • int(4) à long(8) à float(4) à  double(8),整数 à 小数

4.1.2、精度损失问题

  • int(4B)到long(8B),int(4B)到double(8B)都不会有精度损失
  • int(4B)和long(8B)到float(4B),long(8B)到double(8B)会有损失
  • 4B到4B、8B到8B、8B到4B就会有损失(平级、大到小会损失),JVM对这种损失不会抛出异常
  • byte、char、short到int类型转换,实际不存在,因为JVM就把这三者当成int,这样做是为了减少指令,且这三者在都占用栈帧局部变量表中一个槽位slot,也没有必要再区分了

4.2、窄化类型转换

4.2.1、转换规则

  • int到byte、short、char,对应指令i2b、i2s、i2c
  • long到int,对应指令l2i
  • float到int、long,对应指令f2i、f2l
  • double到int、long、float,对应指令d2i、d2l、d2f
  • 除了int,其它类型转为byte、short、char,有两个步骤,先转为int,再转为byte、short、char

4.2.2、精度损失

  • 窄化转换,出现精度损失JVM也不报出异常
  • 如果浮点值是NaN,那么整型或long是0
  • 如果浮点值不是无穷大,那么使用IEEE 745向零舍入模式取整(向下取整),得到整数v
  1. 如果整数v在转换目标整数范围内,那么结果就是v
  2. 如果不在范围内,那么根据正负转为目标范围最大或者最小整数
  • double转为float,要遵守:
  1. 如果转换结果绝对值太小了,无法使用float表示,那么返回float的正负零
  2. 如果转换结果绝对值太大了,无法使用float表示,那么返回float的正无穷大
  3. double的NaN转为float的NaN

5、对象的创建于访问指令

  • 包含实例对象、数组

5.1、创建指令

  • 类实例和数组都是对象
  • 创建对象实例指令:new,接收一个操作数,该操作数是指向常量池的索引,表示要创建的对象的类型,执行完毕以后,将对象的引用压入栈
  • 创建数组的指令:(后面需要跟数组长度,指令出现时出栈)
  1. newarray:创建基本类型数组
  2. anewarrat:创建引用类型数组(数组中的元素是对象/引用类型)
  3. multianewarray:创建多维数组

5.2、字段访问指令

  • 对象创建以后,通过对象访问指令获得对象实例或者数组实例中的字段或者数组元素
  • 访问类字段(static):
  1. getstatic:将字段压入操作数栈
  2. putstatic:将字段弹出栈(字段赋值)
  • 访问类实例字段(非static):
  1. getfield:将字段压入操作数栈
  2. putfield:将字段弹出栈(字段赋值)
  • 举例: getstatic指令,后面会跟一个操作数,指向常量池的索引,该指令的作用就是将索引指定的值压入操作数栈

5.3、数组操作指令

  • 主要有xastore和xaload指令,相对于xstore和xload,多了一个a,因为数组操作相当于引用操作
  1. xastore:将操作数栈的值存储到数组元素中(xstore是存储到布局变量表),有bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore。(举例:xastore在执行前,栈顶需要准备三个元素,分别是值、索引、数组引用,执行时将值存储要引用的数组中指定的索引处)
  2. xaload:将数组元素加载到操作数栈中(xload是将布局变量表中的元素加载到操作数栈中),有baload、caload、saload、iaload、laload、faload、daload、aload(举例:xaload指令要求操作数栈顶元素为数组索引i,次栈顶元素为数组引用a,执行时将弹出栈顶两个元素,然后将a[i]压入栈)

  • 取数组长度指令:arraylenth,将弹出栈顶的数组元素,获取数组的长度,将长度压入栈

5.4、类型检查指令

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

6、方法调用与返回指令

6.1、方法调用指令

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态
  • invokeinterface指令用于调用接口方法
  • invokespecial指令调用特殊处理的实例方法(没有重写的方法),包括初始化方法(构造器)、私有方法和父类方法。都属于静态类型绑定,不会在调用时动态分发
  • invokestatic指令调用类方法(static静态方法),也属于静态绑定,只要有static修饰,就使用invokestatic指令
  • invokedynamic指令,调用动态绑定的方法,JDK1.7后引入(暂不解释)

6.2、方法返回指令

  • 方法的返回指令是由方法定义的返回类型决定的,如果返回的值不是定义的类型,那么会自动转为定义的类型(当然是在可以转的情况下-宽化转型)
  1. 有返回:ireturn(boolean、byte、char、short、int类型均使用ireturn)、lreturn、freturn、dreturn、areturn
  2. 无返回:return (void)
  • 通过返回指令,将当前函数操作栈的栈顶元素弹出,压入调用者函数的操作数栈,然后当前函数操作数栈被抛弃

7.操作数栈管理指令

7.1、定义

  • 操作 操作数栈的指令

7.2、具体指令

  • 将一个或两个元素(一个或两个slot)从栈顶弹出,并直接废弃弹出元素:pop、pop2
  • 复制栈顶一个或两个数值,并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
  1. 不带_x的指令(dup、dup2):复制栈顶元素并压入栈顶,dup后面的系数表示要复制的slot个数,dup复制一个slot,dup2复制两个slot
  2. 带_x的指令(dup_x1、dup2_x1、dup_x2、dup2_x2):复制栈顶元素,并压入栈中指定位置,将dup的系数和x的系数相加,就得到插入的槽位(从被复制的值开始数)
  • 交换栈顶两个slot数值:swap(不能交换64位-long和double)
  • 特殊指令nop,字节码是0x00,和汇编中的nop一样,表示啥也不用干,一般用于调试和占位

8、控制转移指令

  • 包含比较指令、条件跳转指令、多条件分支跳转、无条件跳转(比较指令在算数指令介绍了)

8.1、条件跳转指令

  • 条件跳转指令和比较指令一起使用。在执行条件跳转指令之前,一般先用比较指令进行栈顶元素的准备,然后进行条件跳转。
  • 条件跳转指令有:ifeq、ifne、iflt、ifle、ifgt、ifge,ifnull、ifnonnull。条件跳转指令都接收两个字节的操作数,用于计算跳转位置

  • 指令含义:弹出栈顶元素,测试是否满足某一条件,如果满足则跳转到给定位置
  • boolean、char、byte、short都是使用int比较指令
  • 对于long、float、double类型,先执行相应的比较运算符,比较运算符返回一个整型数到操作数栈,再结合条件跳转指令完成跳转
  • 各类型的比较最终都转换为int类型的比较操作,所以int类型的条件分支指令最丰富

8.2、比较条件跳转指令

  • 类似于将比较指令和条件跳转指令合二为一
  • 指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne。下划线后的i表示int型,a表示引用型;

(输出时,Z表示布尔)

  • 指令后有两个字节的操作数作为参数,用于指定跳转位置,条件满足的跳转,不满足则执行下一条

8.3、多条件分支跳转

  • 多条件分支跳转指令是专门为switch-case指令设计的,主要有tableswitch和lookupswitch

  1. tableswitch要求多个条件分支值是连续的,内部仅存放起始值和终止值,加上几个跳转偏移量,通过操作数index可以定位到跳转偏移量位置,效率较高
  2. lookupswitch内部存放着离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,根据对应的offset值计算跳转的地址,效率较低(为了提升效率,会将case的值排序)
  • JDK1.7以后引入String作为case,比较的时候先比较String的Hash码,Hash码一样的再比较equals方法

8.4、无条件跳转

  • goto语句,后面跟两个字节的操作数,表示要跳转的地方
  • goto_w语句,后面跟四个字节的操作数,表示更大的范围
  • jsr、jsr_w、ret也是无条件跳转指令,主要用于try_finally语句,已经逐渐被抛弃


9、异常处理指令

  • 异常及异常处理:
  1. 过程1:异常对象的生成过程,对应代码throw(手动/自动),对应指令为athrow
  2. 过程2:异常的处理,抓抛模型,对应代码try-catch-finally,指令中使用异常表

9.1、抛出异常指令

  • athrow指令
  1. Java中显式抛出异常(throw)都是由athrow指令实现
  2. 除了显式抛出异常,还有一些是JVM运行时自动检测的,比如除数为0的情况,这个时候是不会有专门的异常指令的,在idiv或ldiv指令时就会抛出异常了
  • 正常情况下,操作数栈的压入和弹出都是一条一条的完成的,唯一例外的就是在异常抛出时,Java虚拟机会清除操作数栈上的所有内容,然后将异常实例压入调用函数的操作栈中

9.2、异常处理与异常表

  • 异常处理:在Java虚拟机中,异常处理(catch)不是由字节码指令实现(早期由jsr、ret指令实现),是有异常表来完成的
  • 异常表:如果一个方法定义了一个try-catch或try-finally异常处理,那么就会创建一个异常表,包含每个异常处理或者finally块的信息,异常表保存了每个异常的处理信息,比如:
  1. 起始位置
  2. 结束位置
  3. 程序计数器记录的代码处理的偏移地址
  4. 被捕获的异常类在常量池中的索引
  • 当一个异常被抛出时,JVM会在当前方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(将异常对象抛给调用函数栈帧)。如果所有栈帧都找不到合适的异常处理,那么这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM终止(比如main线程)
  • 不管什么时候抛出异常,如果异常处理匹配了所有异常类型,代码将继续执行,此时,仍会执行finally块

10、同步控制指令

  • JVM支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,均使用monitor来完成

10.1、方法级的同步

  • 方法级的同步是隐式的,不会在字节码中得到体现,在方法调用和返回的操作中实现。虚拟机从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法
  • 当调用方法时,调用指令将会检查方法的CC_SYNCHRONIZED访问标识是否设置
  1. 如果设置了,执行线程将持有同步锁(会自动加锁),然后执行方法。最后在方法完成时释放同步锁(无论是正常完成还是非正常完成)
  2. 在方法执行期间,执行线程持有了同步锁,任何线程都无法获得同一个锁
  3. 同步方法执行期间抛出异常,且方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放

10.2、​​​​​​​方法内指定指令序列的同步

  • 同步一段指令集序列:对应的代码是java中synchronized语句块。对应的指令集有monitorenter(进入)和monitorexit(退出)
  • 当一个线程进入同步代码块时,使用monitorenter指令请求进入。如果当前对象监视器计数器为0(在对象头Header),那么准许进入,如果计数器为1(监视器计数器++),则判断当前监视器中的线程是否是自己,如果是则进入,如果不是则等待,到计数器为0是进入
  • 退出同步块时,使用monitorexit声明退出(监视器计数器--),在JVM中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态
  • 指令monitorenter和monitorexit在执行时,都需要将对象压入操作数栈顶,之后两条指令的锁定和释放都根据这个对象的监视器monitor来进行
  • 一定要保证monitor监视器计数器++和--正常,所以在字节码后面添加异常指令(任何any异常都可以捕获),一定确保计数器增减正常

JVM系列(十七):字节码指令集相关推荐

  1. 10、字节码指令集与解析举例

    文章目录 第1章.概述 1.执行模型 2.字节码与数据类型 3.指令分析 第2章.加载与存储指令 1.复习:操作数栈与局部变量表 [1]操作数栈(Operand Stacks) [2]局部变量表(Lo ...

  2. 第十九篇_字节码指令集与解析

    文章目录 概述 执行模型 字节码与数据类型 指令分类 加载与存储指令 再谈操作数栈和局部变量表 局部变量压栈指令 常量入栈指令 出栈装入局部变量表 算术运算符指令以及举例 所有的运算符指令 比较指令的 ...

  3. JVM中篇:字节码与类的加载篇

    0.概述 0.1字节码文件的跨平台性 0.1.1.Java语言:跨平台的语言(write once,run anywhere) 当]ava源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再 ...

  4. [三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符

    说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类 只要认真阅读本文必然能够对字节码指令集有所了解 如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范 指令简介 计算机 ...

  5. JVM学习笔记之字节码指令集

    目录 背景 概述 执行模型 字节码与数据类型 指令分类 加载与存储指令 再谈操作数栈与局部变量表 局部变量压栈指令 常量入栈指令 出栈装入局部变量表指令 算术指令 所有算术指令 比较指令的说明 类型转 ...

  6. JVM字节码指令集大全及其介绍

    Java是怎么跨平台的 我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格式计算机才能执行".我们编译后产生的.class ...

  7. Java指令全集_Java的JVM字节码指令集详解

    本文详细介绍了如何使用javap查看java方法中的字节码.以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出.在文末还附有JVM字节码指令集表. 本文不适合没有J ...

  8. 【深入理解JVM】JVM字节码指令集

    Java 虚拟机的指令由一个字节长度的.代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成.虚拟机中许多指令并不包含操作数,只有一 ...

  9. 尚硅谷 宋红康 JVM教程_02_字节码与类的加载篇

    本系列相关链接 尚硅谷 宋红康 JVM教程_01_内存与垃圾回收篇--01 (20210103-20210110) https://blog.csdn.net/wei198621/article/de ...

  10. 29.字节码指令集与解析

    字节码指令集与解析 概述 java字节码对于虚拟机好像汇编语言对于计算机,属于基础执行指令 java虚拟机的指令由一个字节长度代表某种特定操作含义的数字(操作码Opcode)以及跟随其后的零至多个代表 ...

最新文章

  1. 1075 PAT Judge
  2. pandas DataFrame(2)-行列索引及值的获取
  3. mysql的别名可以动态么_mysql别名的使用
  4. 微信小程序:一把瑞士军刀
  5. SAP Fiori Launchpad Contact Support的按钮启用逻辑
  6. Cocos2D中相关问题提问的几个论坛
  7. android磁场传感器页面布局在哪,基于磁场检测的寻线小车传感器布局研究
  8. 使用Notepad++作为Latex编辑器
  9. CSDN下载频道积分规则2.1
  10. vue生命周期的快速记忆方法
  11. 经典英文linux书籍,Linux内核编程必读(英文版),丛书名: 经典原版书库
  12. 5款免费的项目管理软件(推荐收藏)
  13. 小程序顶部自定义标题栏高度自适应
  14. 微信公众号网页授权域名和JS接口安全域名配置攻略
  15. 考 PMP 证书真有用吗?
  16. java文件是什么_java类文件是什么?
  17. 在字节跳动干软件测试5年,4月无情被辞,想给划水的兄弟提个醒
  18. 深度剖析 | 团贷网之死,高利贷团灭简史
  19. Ubuntu 搜狗拼音安装详细步骤
  20. Android 10调用相机拍照

热门文章

  1. 阿里云 vs Azure-安全
  2. IT行业男性出轨率最高!
  3. 天津铸源宝利缘系统成立仪式在津召开
  4. USB 3.0硬件设计
  5. Element table 导出Excel重复数据
  6. 动手学习数据分析第一章内容
  7. 政务服务一网通办建设方案(ppt)
  8. Android GridView实现自定义日程表课表
  9. 电脑无法修改ip地址
  10. success: function(res) {} 和 success: res = {}