【Java 虚拟机笔记】字节码指令相关整理

作者 : 开心源码 本文共15570个字,预计阅读时间需要39分钟 发布时间: 2022-05-12 共222人阅读

文前说明

作为码农中的一员,需要不断的学习,我工作之余将少量分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。

1. 概述

  • Java 虚拟机的指令由 一个字节 长度的、代表着某种特定操作含义的数字(称为 操作码 Opcode)以及跟随其后的零至多个代表此操作所需参数(称为 操作数 Operands)而构成。
    • 因为 Java 虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
    • 因为 Java 字节码的操作码的长度为一个字节(即 0~255),这意味着指令集的操作码总数不可能超过 256 条。
      • 假如要求 Java 运行时所有的数据类型都有对应与之相关的指令去支持的话,操作码的总数将超过 256 条。所以 Java 字节码指令集被设计为 Not Orthogonal(非完全独立),即并非每种数据类型和每种操作都有对应的指令,少量指令可以在必要的时候将少量不被支持的数据类型转换为被支持的数据类型。
        • 在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,如(iload(代表 int)、fload(代表 float)、lload(代表 long)、dload(代表 double)、aload(代表引用数据类型)等)。
        • 通过转换指令,将少量不支持的类型转换成支持的类型,字节码指令本身不支持如 char、byte、short 等基本数据类型(即不包含这些基本数据类型的信息),Java 处理方案是将这些不支持的基本数据类型当成 int 类型解决,减少了很多的指令。
    • 当数据大小超过一个字节时,Java 虚拟机需要重构出具体数据的结构。(比方:将一个 16 位长度的无符号整数使用两个无符号字节(byte1,byte2)存储起来,那它们的值应该是( (byte1<<8)|byte2 ),除了 long 和 double 类型外,每个变量都占局部变量区中的一个变量槽(slot),而 long 及 double 会占用两个连续的变量槽。
  • 字节码指令集的优点。
    • 放弃了操作数长度对齐,意味着可以节省很多填充和间隔符号。
    • 用一个字节代表操作码可以取得尽可能短小精干的编译代码。
  • 字节码指令集的不足。
    • 在解释执行字节码时会损失少量性能。(以时间换空间)

2. 常用的字节码指令

2.1 加载和存储指令

  • 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。(假如将进行运算等操作需要入栈,将局部变量表中的数据入栈加载入操作数栈中,在操作数栈中运算完毕再将结果出栈存储到局部变量表中)
指令说明样例
load 指令将一个局部变量加载到操作栈iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
store 指令将一个数值从操作数栈存储到局部变量表istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
push、dc、const 指令将一个常量加载到操作数栈bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
wide 指令扩充局部变量表的访问索引wide
  • 存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有一些指令,如访问对象的字段或者数组元素的指令也会向操作数栈传输数据。

load 系列指令

  • 把本地变量的送到栈顶。这里的本地变量不仅可以是数值类型,还可以是引用类型。
    • 对于前四个本地变量可以采用 iload_0、iload_1、iload_2、iload_3(分别表示第 0、1、2、3 个整形变量)这种无参简化命令形式。
    • 对本地变量所进行的编号,是对所有类型的本地变量进行的(并不按照类型分类)。
    • 对于非静态函数,第一变量是 this,即对于其的操作是 aload_0。
    • 函数的传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
指令码助记符说明
0x15iload将指定的 int 型本地变量推送至栈顶。
0x16lload将指定的 long 型本地变量推送至栈顶。
0x17fload将指定的 float 型本地变量推送至栈顶。
0x18dload将指定的 double 型本地变量推送至栈顶。
0x19aload将指定的引用类型本地变量推送至栈顶。
0x1aiload_0将第一个 int 型本地变量推送至栈顶。
0x1biload_1将第二个 int 型本地变量推送至栈顶。
0x1ciload_2将第三个 int 型本地变量推送至栈顶。
0x1diload_3将第四个 int 型本地变量推送至栈顶。
0x1elload_0将第一个 long 型本地变量推送至栈顶。
0x1flload_1将第二个 long 型本地变量推送至栈顶。
0x20lload_2将第三个 long 型本地变量推送至栈顶。
0x21lload_3将第四个 long 型本地变量推送至栈顶。
0x22fload_0将第一个 float 型本地变量推送至栈顶。
0x23fload_1将第二个 float 型本地变量推送至栈顶。
0x24fload_2将第三个 float 型本地变量推送至栈顶。
0x25fload_3将第四个 float 型本地变量推送至栈顶。
0x26dload_0将第一个 double 型本地变量推送至栈顶。
0x27dload_1将第二个 double 型本地变量推送至栈顶。
0x28dload_2将第三个 double 型本地变量推送至栈顶。
0x29dload_3将第四个 double 型本地变量推送至栈顶。
0x2aaload_0将第一个引用类型本地变量推送至栈顶。
0x2baload_1将第二个引用类型本地变量推送至栈顶。
0x2caload_2将第三个引用类型本地变量推送至栈顶。
0x2daload_3将第四个引用类型本地变量推送至栈顶。

store 系列指令

  • 把栈顶的值存入本地变量。这里的本地变量不仅可以是数值类型,还可以是引用类型。
    • 对于前四个本地变量可以采用 istore_0、istore_1、istore_2、istore_3(分别表示第 0、1、2、3 个整形变量)这种无参简化命令形式。
    • 对本地变量所进行的编号,是对所有类型的本地变量进行的(并不按照类型分类)。
    • 对于非静态函数,第一变量是 this,它是只读的。
    • 函数传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
指令码助记符说明
0x36istore将栈顶 int 型数值存入指定本地变量。
0x37lstore将栈顶 long 型数值存入指定本地变量。
0x38fstore将栈顶 float 型数值存入指定本地变量。
0x39dstore将栈顶 double 型数值存入指定本地变量。
0x3aastore将栈顶引用型数值存入指定本地变量。
0x3bistore_0将栈顶 int 型数值存入第一个本地变量。
0x3cistore_1将栈顶 int 型数值存入第二个本地变量。
0x3distore_2将栈顶 int 型数值存入第三个本地变量。
0x3eistore_3将栈顶 int 型数值存入第四个本地变量。
0x3flstore_0将栈顶 long 型数值存入第一个本地变量。
0x40lstore_1将栈顶 long 型数值存入第二个本地变量。
0x41lstore_2将栈顶 long 型数值存入第三个本地变量。
0x42lstore_3将栈顶 long 型数值存入第四个本地变量。
0x43fstore_0将栈顶 float 型数值存入第一个本地变量。
0x44fstore_1将栈顶 float 型数值存入第二个本地变量。
0x45fstore_2将栈顶 float 型数值存入第三个本地变量。
0x46fstore_3将栈顶 float 型数值存入第四个本地变量。
0x47dstore_0将栈顶 double 型数值存入第一个本地变量。
0x48dstore_1将栈顶 double 型数值存入第二个本地变量。
0x49dstore_2将栈顶 double 型数值存入第三个本地变量。
0x4adstore_3将栈顶 double 型数值存入第四个本地变量。
0x4bastore_0将栈顶引用型数值存入第一个本地变量。
0x4castore_1将栈顶引用型数值存入第二个本地变量。
0x4dastore_2将栈顶引用型数值存入第三个本地变量。
0x4eastore_3将栈顶引用型数值存入第四个本地变量。

push 系列指令

  • 把一个整形数字(长度比较小)送到到栈顶。命令有一个参数,用于指定要送到栈顶的数字。
    • 命令只能操作肯定范围内的整形数值,超出该范围的使用将使用 ldc 命令。
指令码助记符说明
0x10bipush将单字节的常量值(-128~127)推送至栈顶。
0x11sipush将一个短整型常量值(-32768~32767)推送至栈顶。

dc 系列指令

  • 把数值常量或者 String 常量值从常量池中推送至栈顶。
    • 该命令后面需要给一个表示常量在常量池中位置(编号)的参数。
    • 对于 const 系列和 push 系列指令操作范围之外的数值类型常量,都放在常量池中。
    • 所有不是通过 new 创立的 String 都是放在常量池中的。
指令码助记符说明
0x12ldc将 int,float 或者 String 型常量值从常量池中推送至栈顶。
0x13ldc_w将 int,float 或者 String 型常量值从常量池中推送至栈顶(宽索引)。
0x14ldc2_w将 long 或者 double 型常量值从常量池中推送至栈顶(宽索引)。

const 系列指令

  • 把简单的数值类型送到栈顶。该系列命令不带参数。
    • 只把简单的数值类型送到栈顶时,才使用该命令。
指令码助记符说明
0x02iconst_m1将 int 型(-1)推送至栈顶。
0x03iconst_0将 int 型(0)推送至栈顶。
0x04iconst_1将 int 型(1)推送至栈顶。
0x05iconst_2将 int 型(2)推送至栈顶。
0x06iconst_3将 int 型(3)推送至栈顶。
0x07iconst_4将 int 型(4)推送至栈顶。
0x08iconst_5将 int 型(5)推送至栈顶。
0x09lconst_0将 long 型(0)推送至栈顶。
0x0alconst_1将 long 型(1)推送至栈顶。
0x0bfconst_0将 float 型(0)推送至栈顶。
0x0cfconst_1将 float 型(1)推送至栈顶。
0x0dfconst_2将 float 型(2)推送至栈顶。
0x0edconst_0将 double 型(0)推送至栈顶。
0x0fdconst_1将 double 型(1)推送至栈顶。

2.2 运算指令

  • 运算或者算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
    • 算术指令分为两种:整型运算的指令和浮点型运算的指令。
    • 无论是哪种算术指令,都使用 Java 虚拟机的数据类型,因为没有直接支持 byte、short、char 和 boolean 类型的算术指令,使用操作 int 类型的指令代替。
指令样例
加法指令iadd、ladd、fadd、dadd。
减法指令isub、lsub、fsub、dsub。
乘法指令imul、lmul、fmul、dmul。
除法指令idiv、ldiv、fdiv、ddiv。
求余指令irem、lrem、frem、drem。
取反指令ineg、lneg、fneg、dneg。
位移指令ishl、ishr、iushr、lshl、lshr、lushr。
按位或者指令ior、lor。
按位与指令iand、land。
按位异或者指令ixor、lxor。
局部变量自增指令iinc。
比较指令dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
指令码助记符说明
0x5fswap将栈最顶端的两个数值互换(数值不能是 long 或者 double 类型的)。
0x60iadd将栈顶两 int 型数值相加并将结果压入栈顶。
0x61ladd将栈顶两 long 型数值相加并将结果压入栈顶。
0x62fadd将栈顶两 float 型数值相加并将结果压入栈顶。
0x63dadd将栈顶两 double 型数值相加并将结果压入栈顶。
0x64isub将栈顶两 int 型数值相减并将结果压入栈顶。
0x65lsub将栈顶两 long 型数值相减并将结果压入栈顶。
0x66fsub将栈顶两 float 型数值相减并将结果压入栈顶。
0x67dsub将栈顶两 double 型数值相减并将结果压入栈顶。
0x68imul将栈顶两 int 型数值相乘并将结果压入栈顶。
0x69lmul将栈顶两 long 型数值相乘并将结果压入栈顶。
0x6afmul将栈顶两 float 型数值相乘并将结果压入栈顶。
0x6bdmul将栈顶两 double 型数值相乘并将结果压入栈顶。
0x6cidiv将栈顶两 int 型数值相除并将结果压入栈顶。
0x6dldiv将栈顶两 long 型数值相除并将结果压入栈顶。
0x6efdiv将栈顶两 float 型数值相除并将结果压入栈顶。
0x6fddiv将栈顶两 double 型数值相除并将结果压入栈顶。
0x70irem将栈顶两 int 型数值作取模运算并将结果压入栈顶。
0x71lrem将栈顶两 long 型数值作取模运算并将结果压入栈顶。
0x72frem将栈顶两 float 型数值作取模运算并将结果压入栈顶。
0x73drem将栈顶两 double 型数值作取模运算并将结果压入栈顶。
0x74ineg将栈顶 int 型数值取负并将结果压入栈顶。
0x75lneg将栈顶 long 型数值取负并将结果压入栈顶。
0x76fneg将栈顶 float 型数值取负并将结果压入栈顶。
0x77dneg将栈顶 double 型数值取负并将结果压入栈顶。
0x78ishl将 int 型数值左移位指定位数并将结果压入栈顶。
0x79lshl将 long 型数值左移位指定位数并将结果压入栈顶。
0x7aishr将 int 型数值右(符号)移位指定位数并将结果压入栈顶。
0x7blshr将 long 型数值右(符号)移位指定位数并将结果压入栈顶。
0x7ciushr将 int 型数值右(无符号)移位指定位数并将结果压入栈顶。
0x7dlushr将 long 型数值右(无符号)移位指定位数并将结果压入栈顶。
0x7eiand将栈顶两 int 型数值作 ” 按位与 ” 并将结果压入栈顶。
0x7fland将栈顶两 long 型数值作 ” 按位与 ” 并将结果压入栈顶。
0x80ior将栈顶两 int 型数值作 ” 按位或者 ” 并将结果压入栈顶。
0x81lor将栈顶两 long 型数值作 ” 按位或者 ” 并将结果压入栈顶。
0x82ixor将栈顶两 int 型数值作 ” 按位异或者 ” 并将结果压入栈顶。
0x83lxor将栈顶两 long 型数值作 ” 按位异或者 ” 并将结果压入栈顶。
  • 异常情况:仅规定了在解决整型数据时只有除法指令(idiv、ldiv)以及求余指令(irem、lrem)中出现除数为零时会导致虚拟机 抛出 ArithmeticException 异常,其他任何整型数运算场景都不应该抛出运行时异常。
    • 当一个操作产生溢出时,将会使用有符号的无穷大来表示(NaN)。
    • 在对 long 类型的数据进行比较时,虚拟机采用带符号的比较方式,而在对浮点数值精心比较时,虚拟机采用 IEEE 754 规范所定义的无信号比较方式。

2.3 类型转换指令

  • 类型转换指令可以将两种不同的数值类型进行相互转换。
  • 转换操作一般用于实现客户代码中的显式类型转换操作,或者者用来解决字节码指令集中数据类型相关指令无法与数据类型逐个对应的问题。

宽化类型转换

  • Java 虚拟机天然支持基本数据类型的宽化类型转换。
    • int 类型到 long、float 或者者 double 类型(i2l、i2f、i2d)。
    • long 类型到 float、double 类型(l2f、l2d)。
    • float 类型到 double 类型(f2d)。

窄化类型转换

  • 窄化类型转换必需显式地使用转换指令来完成。
    • 可能会导致转换结果产生不同的正负号、不同的数量级的情况,转化过程很可能会导致数值精度的丢失。直接丢弃多出来的高位
    • 将一个浮点数窄化转换成整数类型 T(T 限于 int 或者 long 类型之一)的时候,将遵循以下转换规则。
      • 假如浮点值是 NaN,那转换结果就是 int 或者 long 中的 0。
      • 假如浮点值不是无穷大的话,浮点值使用 向零舍入模式取整,获取整数值 v,假如 v 在目标 T 的表示范围内,那么结果就是 T;否则将根据 v 的符号,转换为 T 所能表示的最大或者最小整数。
  • Java 虚拟机规范中明确规定数值类型的 窄化转换指令永远不可能导致虚拟机抛出运行时异常
    • 常见的转换指令有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f 等。

2.4 对象创立与访问指令

  • 对于普通对象和数组的创立,Java 虚拟机分别使用了不同的指令去解决。
    • 创立类实例的指令 new。
    • 创立数组的指令 newarray、anewarray、multianewarray。
    • 访问类变量(static 字段)和实例变量(非 static 字段)的指令 getfield、putfield、getstatic、putstatic。
    • 把一个数组元素加载到操作数栈 baload、caload、saload、iaload、laload、faload、daload、aaload。
    • 将一个操作数栈的值存储到数组元素中的指令 bastore、castore、sastore、iastore、fastore、dastore、aastore。
    • 取数组长度的指令 arraylength。
    • 检查普通对象类型的指令 instanceof、checkcast。
  • 把数组的某项送到栈顶。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
指令码助记符说明
0x2eiaload将 int 型数组指定索引的值推送至栈顶。
0x2flaload将 long 型数组指定索引的值推送至栈顶。
0x30faload将 float 型数组指定索引的值推送至栈顶。
0x31daload将 double 型数组指定索引的值推送至栈顶。
0x32aaload将引用型数组指定索引的值推送至栈顶。
0x33baload将 boolean 或者 byte 型数组指定索引的值推送至栈顶。
0x34caload将 char 型数组指定索引的值推送至栈顶。
0x35saload将 short 型数组指定索引的值推送至栈顶。
  • 把栈顶项的值存到数组里。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
指令码助记符说明
0x4fiastore将栈顶 int 型数值存入指定数组的指定索引位置。
0x50lastore将栈顶 long 型数值存入指定数组的指定索引位置。
0x51fastore将栈顶 float 型数值存入指定数组的指定索引位置。
0x52dastore将栈顶 double 型数值存入指定数组的指定索引位置。
0x53aastore将栈顶引用型数值存入指定数组的指定索引位置。
0x54bastore将栈顶 boolean 或者 byte 型数值存入指定数组的指定索引位置。
0x55castore将栈顶 char 型数值存入指定数组的指定索引位置。
0x56sastore将栈顶 short 型数值存入指定数组的指定索引位置。

2.5 操作数栈管理指令

  • 好像操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了少量用于直接操作操作数栈的指令。
指令码助记符说明
0x57pop将栈顶数值弹出(数值不能是 long 或者 double 类型的)。
0x58pop2将栈顶的一个(long 或者 double 类型的)或者两个数值弹出(其它)。
0x59dup复制栈顶数值(数值不能是 long 或者 double 类型的)并将复制值压入栈顶。
0x5adup_x1复制栈顶数值(数值不能是 long 或者 double 类型的)并将两个复制值压入栈顶。
0x5bdup_x2复制栈顶数值(数值不能是 long 或者 double 类型的)并将三个(或者两个)复制值压入栈顶。
0x5cdup2复制栈顶一个(long 或者 double 类型的)或者两个(其它)数值并将复制值压入栈顶。
0x5ddup2_x1复制栈顶数值(long 或者 double 类型的)并将两个复制值压入栈顶。
0x5edup2_x2复制栈顶数值(long 或者 double 类型的)并将三个(或者两个)复制值压入栈顶。

2.6 控制转移指令

  • 控制转移指令可以让 Java 虚拟机有条件或者无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。
    • 从概念模型上了解,可以认为控制转移指令就是在有条件或者无条件地修改 PC 寄存器的值。
      • 条件分支 ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
      • 复合条件分支 tableswitch、lookupswitch。
      • 无条件分支 goto、goto_w、jsr、jsr_w、ret。
  • 在 Java 虚拟机中有专门的指令集用来解决 int 和 reference 类型的条件分支比较操作,为了可以毋庸显著标识一个实体值能否 null,也有专门的指令用来检测 null 值。

2.7 方法调用和返回指令

方法调用指令

  • 前 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由客户所设定的引导方法决定的。
    • invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
    • invokeinterface 指令用于调用接口方法,会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
    • invokespecial 指令用于调用少量需要特殊解决的实例方法,包括实例初始化(<init>)方法、私有方法和父类方法。
    • invokestatic 调用静态方法(static 方法)。
    • invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。

方法返回指令

  • 方法返回指令根据返回值的类型进行区分,包括 ireturn(返回值为 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有一条 return 指令供公告为 void 的方法、实例初始化方法 <init> 以及类和接口的类初始化方法 <clinit> 使用。

关于方法调用

  • Class 文件的编译过程中不包含传统编译中的连接步骤,所有方法调用中的目标方法在 Class 文件里面都是一个常量池中的符号引用,而不是方法在实际运行时内存布局中的入口地址。
  • 在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这类方法(编译期可知,运行期不可变)的调用称为解析(Resolution)。
    • 主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或者别的方式重写其余版本,因而它们都适合在类加载阶段进行解析。
  • 只需能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。
  • 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。

2.8 异常解决指令

  • 在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令实现,除了用 throw 语句显式抛出异常情况之外,Java 虚拟机规范还规定了许多运行时异常会在其余 Java 虚拟机指令检测到异常状况时自动抛出。
    • 例如在整数运算中,当除数为零时,虚拟机会在 idiv 或者 ldiv 指令中抛出 ArithmeticException 异常。
    • 在 Java 虚拟机中,解决异常(catch 语句)不是由字节码指令来实现的(很久之前曾经使用 jsr 和 ret 指令来实现,现在已经不用了),而是采用 异常表 来完成的。

2.9 同步指令

  • Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级同步

  • 方法级的同步:隐式的,即无需通过字节码指令控制,它实现在方法调用和返回操作之中。
    • 当方法调用时,调用指令将会检查方法的 ACC_SYNCRONIZED 访问标志能否被设置,假如是,执行程序就要求先成功持有管程,而后才能执行方法,最后当方法执行完成时(无论是正常完成还是非正常完成)时释放管程。
    • 在方法执行期间,执行线程持有了管程,其余任何线程都无法再获取到同一个管程。
    • 假如执行期间出现了方法内部无法处理的异常,那么这个方法所持有的管程将在异常抛出到同步方法之外时自动释放。

方法内部一段指令序列的同步

  • 同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正的确现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。
    • 编译器必需保证无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必需执行其对应的 monitorexit 指令,而无论这个方法时正常结束还是异常结束。

2.10 程序执行使用样例

public class Test {    public static int minus(int x) {        return -x;    }    public static void main(String[] args) {        int x = 5;        int y = minus(x);    }}
  • 从固化在 Class 文件中的二进制字节码开始,经过加载器对当前类的加载,虚拟机对二进制码的验证、准备和肯定的解析,进入内存中的方法区,常量池中的符号引用肯定程度上转换为直接引用,使得字节码通过结构化的组织让虚拟机理解类的每一块的构成,创立的线程申请到了虚拟机栈中的空间构造出属于这一线程的栈帧空间。
{  public services.Test();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 3: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   Lservices/Test;  public static int minus(int);    descriptor: (I)I    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=1, args_size=1         0: iload_0         1: ineg         2: ireturn      LineNumberTable:        line 6: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       3     0     x   I  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=3, args_size=1         0: iconst_5         1: istore_1         2: iload_1         3: invokestatic  #2                  // Method minus:(I)I         6: istore_2         7: return      LineNumberTable:        line 10: 0        line 11: 2        line 12: 7      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       8     0  args   [Ljava/lang/String;            2       6     1     x   I            7       1     2     y   I}
  • 检查 main() 方法的访问标志(ACC_PUBLIC,ACC_STATIC)、形容符形容的返回类型和参数列表,确定可以访问后进入 Code 属性表执行命令,读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数数向局部变量表中依序加入参数(第一个参数是引用当前对象的 this,所以空参数列表的参数数量也是 1)。
public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=3, args_size=1         0: iconst_5         1: istore_1         2: iload_1         3: invokestatic  #2                  // Method minus:(I)I         6: istore_2         7: return      LineNumberTable:        line 10: 0        line 11: 2        line 12: 7      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       8     0  args   [Ljava/lang/String;            2       6     1     x   I            7       1     2     y   I
  • 将整数 5 压入栈顶。
0: iconst_5
  • 将栈顶整数值存入局部变量表的 slot1(slot0 是参数 this)。
1: istore_1
  • 将 slot1 压入栈顶。
2: iload_1
  • invokestatic 指令用于调用静态方法,参数是根据常量池中已经转换为直接引用的常量,即 minus() 函数在方法区中的地址,找到这个地址调用函数,向其中加入的参数为栈顶的值。
3: invokestatic  #2                  // Method minus:(I)I
  • 将栈顶整数存入局部变量的 slot2。
6: istore_2
  • 将返回地址中存储的 PC 地址返到 PC,栈帧恢复到调用前。
7: return
  • minus() 函数执行过程,同样的首先检查函数的访问标志、形容符形容的返回类型和参数列表,确定可以访问后进入 Code 属性表执行命令,读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数数向局部变量表中依序加入参数,而后开始根据命令正式执行。
public static int minus(int);    descriptor: (I)I    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=1, args_size=1         0: iload_0         1: ineg         2: ireturn      LineNumberTable:        line 6: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       3     0     x   I
  • 将 slot0 压入栈顶,也就是传入的参数。
 0: iload_0
  • 将栈顶的值弹出取负后压回栈顶。
1: ineg
  • 将返回地址中存储的 PC 地址返到 PC,栈帧恢复到调用前。
2: ireturn
  • 从二进制字节码里可以看到 invokestatic 指令调用的是 minus() 方法的直接引用,在编译期这个调用就已经决定了。假如方法是动态绑定,在编译期并不知道使用哪个方法(或者者是不知道使用方法的哪个版本),那么这个时候就需要在运行时才能确定哪个版本的方法将被调用,这个时候才能将符号引用转换为直接引用。这个问题提到的多个版本的方法与 Java 中的 重载多态重写 问题息息相关。

重载(override)

public class Test {    static class Human {    }    static class Man extends Human {    }    static class Woman extends Human {    }    public void sayHello(Human human) {        System.out.println("hello human");    }    public void sayHello(Man man) {        System.out.println("hello man");    }    public void sayHello(Woman woman) {        System.out.println("hello woman");    }    public static void main(String[] args) {        Test demo = new Test();        Human man = new Man();        Human woman = new Woman();        demo.sayHello(man);        demo.sayHello(woman);    }}/*print hello humanhello human*/
  • 在重载中,程序调用的是参数实际类型不同的方法,但是虚拟机最终分派了相同外观类型(静态类型)的方法,这说明在重载的过程中虚拟机在运行的时候是只看参数的外观类型(静态类型)的,而这个外观类型(静态类型)是在编译的时候就已经确定,和虚拟机没有关系。
    • 这种依赖静态类型来做方法的分配叫做 静态分派
public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=4, args_size=1         0: new           #7                  // class services/Test         3: dup         4: invokespecial #8                  // Method "<init>":()V         7: astore_1         8: new           #9                  // class services/Test$Man        11: dup        12: invokespecial #10                 // Method services/Test$Man."<init>":()V        15: astore_2        16: new           #11                 // class services/Test$Woman        19: dup        20: invokespecial #12                 // Method services/Test$Woman."<init>":()V        23: astore_3        24: aload_1        25: aload_2        26: invokevirtual #13                 // Method sayHello:(Lservices/Test$Human;)V        29: aload_1        30: aload_3        31: invokevirtual #13                 // Method sayHello:(Lservices/Test$Human;)V        34: return      LineNumberTable:        line 29: 0        line 30: 8        line 31: 16        line 32: 24        line 33: 29        line 34: 34      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      35     0  args   [Ljava/lang/String;            8      27     1  demo   Lservices/Test;           16      19     2   man   Lservices/Test$Human;           24      11     3 woman   Lservices/Test$Human;

重写(overwrite)

public class Test {    static class Human {        public void sayHello() {            System.out.println("hello human");        }    }    static class Man extends Human {        public void sayHello() {            System.out.println("hello man");        }    }    static class Woman extends Human {        public void sayHello() {            System.out.println("hello woman");        }    }    public static void main(String[] args) {        Human man = new Man();        Human woman = new Woman();        man.sayHello();        woman.sayHello();    }}/*printhello manhello woman*/
  • 在重写中,程序调用的是不同实际类型的同名方法,虚拟机依据对象的实际类型去寻觅能否有这个方法,假如有就执行,假如没有去父类里找,最终在实际类型里找到了这个方法,所以最终是在运行期动态分派了方法。
    • 在编译的时候可以看到字节码指示的方法都是一样的符号引用,但是运行期虚拟机能够根据实际类型去确定出真正需要的直接引用。
    • 这种依赖实际类型来做方法的分配叫做 动态分派
    • 得益于 Java 虚拟机的动态分派会在分派前确定对象的实际类型,面向对象的多态性才能表现出来。
public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=3, args_size=1         0: new           #2                  // class services/Test$Man         3: dup         4: invokespecial #3                  // Method services/Test$Man."<init>":()V         7: astore_1         8: new           #4                  // class services/Test$Woman        11: dup        12: invokespecial #5                  // Method services/Test$Woman."<init>":()V        15: astore_2        16: aload_1        17: invokevirtual #6                  // Method services/Test$Human.sayHello:()V        20: aload_2        21: invokevirtual #6                  // Method services/Test$Human.sayHello:()V        24: return      LineNumberTable:        line 24: 0        line 25: 8        line 26: 16        line 27: 20        line 28: 24      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      25     0  args   [Ljava/lang/String;            8      17     1   man   Lservices/Test$Human;           16       9     2 woman   Lservices/Test$Human;

参考资料

https://www.songma.com/p/d95cfde7fc49
https://blog.csdn.net/Alexwym/article/details/82152665

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【Java 虚拟机笔记】字节码指令相关整理

发表回复