跳至主要內容

字节码指令集

会敲代码的程序猿原创JVMJVM大约 9 分钟

字节码指令集

字节码指令集是Java虚拟机(JVM)能理解和执行的低级指令集合。具体保存在Java类文件(.class)的方法区部分,由操作码和操作数组成。

  • 操作码(Opcode): 一个字节长度的数字,代表某种特定操作
  • 操作数(Operands): 跟随操作码之后的零至多个参数,用于该操作所需的数据

由于JVM采用面向操作数栈而不是面向寄存器的架构,大多数指令都不包含操作数,只有一个操作码,指令参数存放在操作数栈中。

操作码助记符

数据类型相关的字节码指令,包含特定的操作码助记符

数据类型操作码助记符
inti
longl
shorts
byteb
charc
floatf
doubled
referencea

也有一些指令没有明确的类型字符:

指令描述
arraylength操作数为数组类型对象
goto无条件跳转指令,与数据类型无关

由于操作码长度只有一个字节,如果每种类型的指令都支持所有数据类型,指令数量将超出范围。 因此,Java虚拟机的指令集设计成非完全独立的(“Not Orthogonal”)。 即并非每种数据类型和每一种操作都有对应的指令。

操作码表

使用数据类型对应的操作码助记符替换操作码opcode列指令模板中的T,得到具体的字节码指令。

参考下表Java虚拟机指令集所支持的数据类型

opcodebyteshortintlongfloatdoublecharreference
Tpushbipushsipush
Tconsticonstlconstfconstdconstaconst
Tloadiloadlloadfloaddloadaload
Tstoreistorelstorefstoredstoreastore
Tinciinc
Taloadbaloadsaloadialoadlaloadfaloaddaloadcaload
Tastorebastoresastoreiastorelastorefastoredastorecastore
Taddiaddladdfadddadd
Tsubisublsubfsubdsub
Tmulimullmulfmuldmul
Tdividivldivfdivddiv
Tremiremlremfremdrem
Tnegineglnegfnegdneg
Tshlishllshl
Tshrishrlshr
Tushriushrlushr
Tandiandland
Toriorlor
Txorixorlxor
i2Ti2bi2si2ii2li2fi2d
l2Tl2il2ll2fl2d
f2Tf2if2lf2ff2d
d2Td2id2ld2fd2d
Tcmpicmplcmp
Tcmplfcmpldcmpl
Tcmpgfcmpgdcmpg
if_TcmpOPif_icmpOPif_acmpOP
Treturnireturnlreturnfreturndreturnareturn

从表中看来,大部分指令不支持bytecharshort类型,boolean类型更是没有任何指令支持。

  • 编译器会在编译期或运行期将byteshort类型数据带符号扩展(Sign-Extend)为int类型
  • booleanchar类型数据零位扩展(Zero-Extend)为int类型
  • 在处理这些类型的数组时,也会转换为使用int类型的字节码指令

因此,大多数对booleanbyteshortchar类型数据的操作,实际上都是使用int类型进行的。

字节码指令分类

加载和存储指令

加载和存储指令用于在栈帧中的局部变量表和操作数栈之间传输数据。

  • 将局部变量加载到操作数栈
    • 整数加载指令:iloadiload_<n>
    • 长整型加载指令:lloadlload_<n>
    • 浮点型加载指令:floadfload_<n>
    • 双精度浮点型加载指令:dloaddload_<n>
    • 引用类型加载指令:aloadaload_<n>
  • 将数值从操作数栈存储到局部变量表
    • 整数存储指令:istoreistore_<n>
    • 长整型存储指令:lstorelstore_<n>
    • 浮点型存储指令:fstorefstore_<n>
    • 双精度浮点型存储指令:dstoredstore_<n>
    • 引用类型存储指令:astoreastore_<n>
  • 将常量加载到操作数栈
    • 字节常量加载指令:bipush
    • 短整型常量加载指令:sipush
    • 常量池加载指令:ldcldc_wldc2_w
    • 空常量加载指令:aconst_null
    • 整数常量加载指令:iconst_m1iconst_<i>
    • 长整型常量加载指令:lconst_<l>
    • 浮点型常量加载指令:fconst_<f>
    • 双精度浮点型常量加载指令:dconst_<d>
  • 扩充局部变量表访问索引的指令
    • 扩展索引指令:wide

加载和存储指令主要用于操作数栈和局部变量表之间的数据传输。 此外,一些指令(如访问对象字段或数组元素的指令)也会涉及操作数栈的数据传输。

运算指令

算术指令用于对两个操作数栈上的值进行特定运算,并将结果重新存入到操作栈顶。

  • 算术指令列表:
    • 加法指令:iaddladdfadddadd
    • 减法指令:isublsubfsubdsub
    • 乘法指令:imullmulfmuldmul
    • 除法指令:idivldivfdivddiv
    • 求余指令:iremlremfremdrem
    • 取反指令:ineglnegfnegdneg
    • 位移指令:ishlishriushrlshllshrlushr
    • 按位或指令:iorlor
    • 按位与指令:iandland
    • 按位异或指令:ixorlxor
    • 局部变量自增指令:iinc
    • 比较指令:dcmpgdcmplfcmpgfcmpllcmp

类型转换指令

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

  • 宽化类型转换: 即小范围类型向大范围类型的安全转换
    • int类型到longfloat或者double类型
    • long类型到floatdouble类型
    • float类型到double类型
  • 窄化类型转换: 与“宽化”相对,需显式指令,可能导致正负号变化和精度丢失
    • i2bi2ci2sl2if2if2ld2id2ld2f

对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。 对象创建后,可以通过对象访问指令来获取对象实例或数组中的字段或者数组元素。

  • 对象创建指令
    • 创建类实例:new
    • 创建数组:newarrayanewarraymultianewarray
  • 对象访问指令
    • 访问字段:getfieldputfieldgetstaticputstatic
    • 访问数组元素:baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
    • 存储数组元素:bastorecastoresastoreiastorefastoredastoreaastore
    • 数组操作:arraylength
    • 类型检查和转换:instanceofcheckcast

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  • 操作数栈管理指令
    • 出栈:poppop2
    • 复制栈顶元素:dupdup2dup_x1dup2_x1dup_x2dup2_x2
    • 互换栈顶两个元素:swap

控制转移指令

控制转移指令用于在程序执行过程中有条件或无条件地跳转到其他指令位置,修改程序计数器(PC)的值。

  • 条件分支
    • ifeqifltifleifneifgtifge
    • ifnullifnonnull
    • if_icmpeqif_icmpneif_icmpltif_icmpgtif_icmpleif_icmpge
    • if_acmpeqif_acmpne
  • 复合条件分支
    • tableswitch — 使用表的方式处理范围内的分支
    • lookupswitch — 使用查找表的方式处理分支
  • 无条件分支
    • gotogoto_w — 无条件跳转到指定位置
    • jsrjsr_w — 跳转到子程序并保存返回地址
    • ret — 从子程序返回

所有比较最终都转为int类型,Java虚拟机提供了丰富的 int 类型条件分支指令:

  • booleanbytecharshort直接使用int类型指令
  • longfloatdouble先用对应比较指令,再转换为int进行条件分支

方法调用和返回指令

  • 方法调用(分派、执行过程) 分为以下五种指令,用于不同类型的方法调用:
    • invokevirtual:调用对象的实例方法,依据对象的实际类型进行分派(虚方法分派),最常见
    • invokeinterface:调用接口方法,运行时搜索实现了接口的方法
    • invokespecial:调用需要特殊处理的实例方法,如实例初始化方法、私有方法和父类方法
    • invokestatic:调用类静态方法(static方法)
    • invokedynamic:运行时动态解析和调用方法,分派逻辑由用户定义
  • 方法返回指令 根据返回值的类型区分,包括:
    • ireturn:返回 booleanbytecharshortint 类型的值
    • lreturn:返回 long 类型的值
    • freturn:返回 float 类型的值
    • dreturn:返回 double 类型的值
    • areturn:返回引用类型的值
    • return:用于声明为 void 的方法、实例初始化方法、类初始化方法

异常处理指令

在Java程序中,显式抛出异常throw 语句)由athrow指令实现。

除了显式抛出异常,在JVM指令检测到异常状况时,会自动抛出运行时异常。 例如,在整数运算中,当除数为零时,虚拟机会在idivldiv指令中抛出ArithmeticException异常。

处理异常(catch 语句)在Java虚拟机中不是通过字节码指令实现的,而是采用异常表来完成。

同步指令

Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,通常称为“锁”)来实现的。

方法级同步

方法级同步是隐式的,通过方法调用和返回操作实现。 虚拟机从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志来判断方法是否被声明为同步方法。同步方法的执行过程如下:

  1. 方法调用时,检查 ACC_SYNCHRONIZED 标志。
  2. 如果设置了该标志,执行线程需先成功持有管程,然后才能执行方法。
  3. 方法执行完成(无论正常还是异常)后,释放管程。
  4. 在方法执行期间,持有管程的线程独占管程,其他线程无法获取同一个管程。

指令序列同步

同步一段指令集序列由 synchronized 语句块表示。Java虚拟机的指令集中有 monitorentermonitorexit 两条指令来支持 synchronized 关键字的语义。以下是一个示例代码及其编译后的字节码序列:

void onlyMe(Foo f) {
    synchronized(f) {
        doSomething();
    }
}

编译后的字节码序列:

Method void onlyMe(Foo)
0  aload_1       // 将对象f入栈
1  dup           // 复制栈顶元素(即f的引用)
2  astore_2      // 将栈顶元素存储到局部变量表变量槽 23  monitorenter  // 以栈顶元素(即f)作为锁,开始同步
4  aload_0       // 将局部变量槽 0(即this指针)的元素入栈
5  invokevirtual #5 // 调用doSomething()方法
8  aload_2       // 将局部变量槽 2的元素(即f)入栈
9  monitorexit   // 退出同步
10 goto 18       // 方法正常结束,跳转到18返回
13 astore_3      // 异常路径起始,见下面异常表的Target 13
14 aload_2       // 将局部变量槽 2的元素(即f)入栈
15 monitorexit   // 退出同步
16 aload_3       // 将局部变量槽 3的元素(即异常对象)入栈
17 athrow        // 把异常对象重新抛出给onlyMe()方法的调用者
18 return        // 方法正常返回

Exception table:
From    To  Target  Type
    4       10  13      any
    13      16  13      any

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都有其对应的monitorexit指令,无论是正常结束还是异常结束。 为了保证在方法异常完成时monitorentermonitorexit指令依然正确配对执行,编译器会自动生成一个异常处理程序,用于执行monitorexit指令。