欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

虚拟机字节码指令集

程序员文章站 2022-07-13 14:11:30
...
我们都知道 *.java 源代码经过编译器编译后会生成 *.class 文件,方法体中的代码会存放在方法表中 Code 属性中(接口和抽象类除外,没有 Code 属性).

Code 属性的结构:

attribute_name_index -> UTF8(Code)
attribute_length
info{
    max_stack
    max_locals
    code_length
    code
    LineNumberTable
    LocalVariableTable
    ...
}

java 编译器编译后的字节码指令就存放在 code 中.



那么下面我们重点专注下虚拟机的字节码.

我们知道指令是由操作码和操作数组成的,java 虚拟机定义的操作码由一字节表示,这样意味着最多只有256个操作码. class 文件放弃了操作数对齐,那么对于16字节的无符号数,虚拟机必须在运行时重构出该数据,类似 byte1 << 8 | byte2 这样. 所以其会牺牲一部分性能,但是好处同样是显而易见的.不需要多操作数进行补位填充,节省了空间.

java 虚拟机是面向操作数栈而不是寄存器的,所以它的指令大多数是不带操作数的. java 操作码大多都带有特定含义.

下面说下虚拟机是如何工作的:

do{
    PC寄存器自动+1
    根据寄存器位置读出操作码
    if(判断操作码后是否有操作数) 读取操作数
    执行相关功能
}while(字节码长度>0)

1.从局部变量读取数据到操作数栈
iload/lload/fload 等
2.把操作数栈的数据放回局部变量表
istore/lstore/fstore 等.
3.把常量读取到操作数栈
bipush/sipush/ldc 等
4.扩充局部变量表访问索引的指令 wide

注:iload_n 代表 iload_0 iload_1 iload_2 iload_3 这几条指令.

运算指令:用于把操作数栈上的两个数进行某种特定运算,然后把结果重新放回栈顶的操作.
1.加法
iadd/ladd/dadd 等
2.减法
isub/lsub/dsub
3.乘法
imul/lmul/dmul
4.除法
idiv/ldiv/ddiv
5.取余
irem/drem
6.or
ior/dor
7.and
iand/dand
8.取反
ineg/dneg
9.局部变量自增指令
iinc
10.比较指令
dcmpg 等

我们知道,两个很大的数据相加,最终会得到负数,这个在纯数学领域是不会的,但是在计算机中是会的(由于溢出),java 虚拟机并没有规定这个错误,只是在进行 div 或 rem 运算的时候,如果除数为 0,则会抛出 ArithmeticException 异常,其余任何整型数运算场景都不应该抛出运行时异常.

显式类型转换:i2b,i2c,l2i等.
隐式类型转换:i ->long/float/double
                         long -> float/double
                         float -> double


创建对象和访问指令

1.创建一个对象:new
2.创建数组:
    ① newarray 创建基本数据类型的数组
    ② anewarray 创建对象类型数组
    ③ multianewarray 创建多维数组
3.访问类字段:getfield、putfield、getstatic、putstatic
4.把一个元素数组加载到操作数栈:iaload、baload、laload 等
5.把一个操作数栈中的值存储到数组元素:iastore、lastore 等
6.取数组长度的指令:arraylength
7.检查 instanceof、checkcast

操作数栈管理指令
出栈:pop、pop2
复制栈顶元素:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
交换:swap

控制转移指令:就是有条件的或无条件的修改 PC 寄存器的值.

1.条件分支:ifeq、iflt 等
2.复合条件分支:tableswitch、lookupswitch
3.无条件分支:goto 等

方法调用和返回指令

(1)invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)
(2)invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用.
(3)invokespeical 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法.
(4)invokestatic 指令用于调用类方法(static 方法)
(5)invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在 Java 虚拟机
内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的.

方法调用指令与数据类型无关,而方法返回指令时根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short、int
类型时使用)、lreturn、freturn、dreturn、areturn,另外还有一条 return 指令供声明为 void 的方法、实例初始化方法以及类和接口
的类初始化方法使用.

异常处理指令

在 Java 程序中显示抛出异常的操作(throw 语句)都由 athrow 指令实现,除了用 throw 语句显式抛出异常情况之外,Java 虚拟机规范还规定
了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出.

在 Java 虚拟机中,处理异常 (catch 语句) 不是由字节码指令来实现的(很久之前曾经使用 jsr 和 ret 指令来实现,现在已经不用了),而
是采用异常表来完成.

同步指令

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

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中. 虚拟机可以从方法常量池的方法表结构中的
ACC_SYNCHRONIZED 访问标志得知一个方法是否是同步方法. 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置
了,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程.

在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程. 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理
此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放.

关于同步代码块,虚拟机中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 语义. 正确实现 synchrnoized 关键字
需要 javac 编译器与 java 虚拟机两者共同协作支持.
monitorenter 和 monitorexit 指令底层是 通过lock 指令来实现的,在执行 monitorexit 之前,会将数据刷新会主存.

注:编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必须执行器对应的 monitorexit 指令,而无论这个方法
是正常结束还是异常结束.

换句话说,monitorenter 和 monitorexit 成对出现.

公有设计和私有实现

Java 虚拟机规范描绘了 Java 虚拟机应有的共同程序存储格式:Class 文件格式以及字节码指令集.

虚拟机实现的方式主要有一下两种:

(1)将输入的 Java 虚拟机代码在加载或执行时翻译成另一种虚拟机的指令集(*.java -> *.class)
(2)将输入的 Java 虚拟机代码在加载或执行时翻译成宿主机 CPU 的本地指令集(即 JIT 代码生成技术)(*.java -> 目标指令集)


Class 文件结构的发展

Class 文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是 Java 技术体系实现平台无关、语言无关两项
特性的重压支柱.

字节码指令集(参考:https://www.cnblogs.com/cwane/p/6097838.html):
Opcode
Mnemonics
Note
Constants
0x00
nop
无动作
0x01
aconst_null
把 null 推到操作数栈
0x02
iconst_m1
把 int 常量 –1 推到操作数栈
0x03
iconst_0
把 int 常量 0 推到操作数栈
0x04
iconst_1
把 int 常量 1 推到操作数栈
0x05
iconst_2
把 int 常量 2 推到操作数栈
0x06
iconst_3
把 int 常量 3 推到操作数栈
0x07
iconst_4
把 int 常量 4 推到操作数栈
0x08
iconst_5
把 int 常量 5 推到操作数栈
0x09
lconst_0
把 long 常量 0 推到操作数栈
0x0A
lconst_1
把 long 常量 1 推到操作数栈
0x0B
fconst_0
把 float 常量 0 推到操作数栈
0x0C
fconst_1
把 float 常量 1 推到操作数栈
0x0D
fconst_2
把 float 常量 2 推到操作数栈
0x0E
dconst_0
把 double 常量 0 推到操作数栈
0x0F
dconst_1
把 double 常量 1 推到操作数栈
0x10
bipush
把单字节常量(-128~127)推到操作数栈
0x11
sipush
把 short 常量(-32768~32767)推到操作数栈
0x12
ldc
把常量池中的int,float,String型常量取出并推到操作数栈
0x13
ldc_w
把常量池中的int,float,String型常量取出并推到操作数栈(宽索引)
0x14
ldc2_w
把常量池中的long,double型常量取出并推到操作数栈(宽索引)
Loads
0x15
iload
把 int 型局部变量推到操作数栈
0x16
lload
把 long 型局部变量推到操作数栈
0x17
fload
把 float 型局部变量推到操作数栈
0x18
dload
把 double 型局部变量推到操作数栈
0x19
aload
把引用型局部变量推到操作数栈
0x1A
iload_0
把局部变量第 1 个 int 型局部变量推到操作数栈
0x1B
iload_1
把局部变量第 2 个 int 型局部变量推到操作数栈
0x1C
iload_2
把局部变量第 3 个 int 型局部变量推到操作数栈
0x1D
iload_3
把局部变量第 4 个 int 型局部变量推到操作数栈
0x1E
lload_0
把局部变量第 1 个 long 型局部变量推到操作数栈
0x1F
lload_1
把局部变量第 2 个 long 型局部变量推到操作数栈
0x20
lload_2
把局部变量第 3 个 long 型局部变量推到操作数栈
0x21
lload_3
把局部变量第 4 个 long 型局部变量推到操作数栈
0x22
fload_0
把局部变量第 1 个 float 型局部变量推到操作数栈
0x23
fload_1
把局部变量第 2 个 float 型局部变量推到操作数栈
0x24
fload_2
把局部变量第 3 个 float 型局部变量推到操作数栈
0x25
fload_3
把局部变量第 4 个 float 型局部变量推到操作数栈
0x26
dload_0
把局部变量第 1 个 double 型局部变量推到操作数栈
0x27
dload_1
把局部变量第 2 个 double 型局部变量推到操作数栈
0x28
dload_2
把局部变量第 3 个 double 型局部变量推到操作数栈
0x29
dload_3
把局部变量第 4 个 double 型局部变量推到操作数栈
0x2A
aload_0
把局部变量第 1 个引用型局部变量推到操作数栈
0x2B
aload_1
把局部变量第 2 个引用型局部变量推到操作数栈
0x2C
aload_2
把局部变量第 3 个引用型局部变量推到操作数栈
0x2D
aload_3
把局部变量第 4 个引用 型局部变量推到操作数栈
0x2E
iaload
把 int 型数组指定索引的值推到操作数栈
0x2F
laload
把 long 型数组指定索引的值推到操作数栈
0x30
faload
把 float 型数组指定索引的值推到操作数栈
0x31
daload
把 double 型数组指定索引的值推到操作数栈
0x32
aaload
把引用型数组指定索引的值推到操作数栈
0x33
baload
把 boolean或byte型数组指定索引的值推到操作数栈
0x34
caload
把 char 型数组指定索引的值推到操作数栈
0x35
saload
把 short 型数组指定索引的值推到操作数栈
Stores
0x36
istore
把栈顶 int 型数值存入指定局部变量
0x37
lstore
把栈顶 long 型数值存入指定局部变量
0x38
fstore
把栈顶 float 型数值存入指定局部变量
0x39
dstore
把栈顶 double 型数值存入指定局部变量
0x3A
astore
把栈顶引用型数值存入指定局部变量
0x3B
istore_0
把栈顶 int 型数值存入第 1 个局部变量
0x3C
istore_1
把栈顶 int 型数值存入第 2 个局部变量
0x3D
istore_2
把栈顶 int 型数值存入第 3 个局部变量
0x3E
istore_3
把栈顶 int 型数值存入第 4 个局部变量
0x3F
lstore_0
把栈顶 long 型数值存入第 1 个局部变量
0x40
lstore_1
把栈顶 long 型数值存入第 2 个局部变量
0x41
lstore_2
把栈顶 long 型数值存入第 3 个局部变量
0x42
lstore_3
把栈顶 long 型数值存入第 4 个局部变量
0x43
fstore_0
把栈顶 float 型数值存入第 1 个局部变量
0x44
fstore_1
把栈顶 float 型数值存入第 2 个局部变量
0x45
fstore_2
把栈顶 float 型数值存入第 3 个局部变量
0x46
fstore_3
把栈顶 float 型数值存入第 4 个局部变量
0x47
dstore_0
把栈顶 double 型数值存入第 1 个局部变量
0x48
dstore_1
把栈顶 double 型数值存入第 2 个局部变量
0x49
dstore_2
把栈顶 double 型数值存入第 3 个局部变量
0x4A
dstore_3
把栈顶 double 型数值存入第 4 个局部变量
0x4B
astore_0
把栈顶 引用 型数值存入第 1 个局部变量
0x4C
astore_1
把栈顶 引用 型数值存入第 2 个局部变量
0x4D
astore_2
把栈顶 引用 型数值存入第 3 个局部变量
0x4E
astore_3
把栈顶 引用 型数值存入第 4 个局部变量
0x4F
iastore
把栈顶 int 型数值存入数组指定索引位置
0x50
lastore
把栈顶 long 型数值存入数组指定索引位置
0x51
fastore
把栈顶 float 型数值存入数组指定索引位置
0x52
dastore
把栈顶 double 型数值存入数组指定索引位置
0x53
aastore
把栈顶 引用 型数值存入数组指定索引位置
0x54
bastore
把栈顶 boolean or byte 型数值存入数组指定索引位置
0x55
castore
把栈顶 char 型数值存入数组指定索引位置
0x56
sastore
把栈顶 short 型数值存入数组指定索引位置
Stack
0x57
pop
把栈顶数值弹出(非long,double数值)
0x58
pop2
把栈顶的一个long或double值弹出,或弹出2个其他类型数值
0x59
dup
复制栈顶数值并把数值入栈
0x5A
dup_x1
复制栈顶数值并把数值入栈
0x5B
dup_x2
0x5C
dup2
0x5D
dup2_x1
0x5E
dup2_x2
0x5F
swap
把栈顶端的两个数的值交换
Math
0x60
iadd
把栈顶两个 int 型数值相加并将结果入栈
0x61
ladd
把栈顶两个 long 型数值相加并将结果入栈
0x62
fadd
把栈顶两个 float 型数值相加并将结果入栈
0x63
dadd
把栈顶两个 double 型数值相加并将结果入栈
0x64
isub
把栈顶两个 int 型数值相减并将结果入栈
0x65
lsub
把栈顶两个 long 型数值相减并将结果入栈
0x66
fsub
把栈顶两个 float 型数值相减并将结果入栈
0x67
dsub
把栈顶两个 double 型数值相减并将结果入栈
0x68
imul
把栈顶两个 int 型数值相乘并将结果入栈
0x69
lmul
把栈顶两个 long 型数值相乘并将结果入栈
0x6A
fmul
把栈顶两个 float 型数值相乘并将结果入栈
0x6B
dmul
把栈顶两个 double 型数值相乘并将结果入栈
0x6C
idiv
把栈顶两个 int 型数值相除并将结果入栈
0x6D
ldiv
把栈顶两个 long 型数值相除并将结果入栈
0x6E
fdiv
把栈顶两个 float 型数值相除并将结果入栈
0x6F
ddiv
把栈顶两个 double 型数值相除并将结果入栈
0x70
irem
把栈顶两个 int 型数值模运算并将结果入栈
0x71
lrem
把栈顶两个 long 型数值模运算并将结果入栈
0x72
frem
把栈顶两个 float 型数值模运算并将结果入栈
0x73
drem
把栈顶两个 double 型数值模运算并将结果入栈
0x74
ineg
把栈顶 int 型数值取负并将结果入栈
0x75
lneg
把栈顶 long 型数值取负并将结果入栈
0x76
fneg
把栈顶 float 型数值取负并将结果入栈
0x77
dneg
把栈顶 double 型数值取负并将结果入栈
0x78
ishl
把 int 型数左移指定位数并将结果入栈
0x79
lshl
把 long 型数左移指定位数并将结果入栈
0x7A
ishr
把 int 型数右移指定位数并将结果入栈(有符号)
0x7B
lshr
把 long 型数右移指定位数并将结果入栈(有符号)
0x7C
iushr
把 int 型数右移指定位数并将结果入栈(无符号)
0x7D
lushr
把 long 型数右移指定位数并将结果入栈(无符号)
0x7E
iand
把栈顶两个 int 型数值 按位与 并将结果入栈
0x7F
land
把栈顶两个 long 型数值 按位与 并将结果入栈
0x80
ior
把栈顶两个 int 型数值 按位或 并将结果入栈
0x81
lor
把栈顶两个 long 型数值 按或与 并将结果入栈
0x82
ixor
把栈顶两个 int 型数值 按位异或 并将结果入栈
0x83
lxor
把栈顶两个 long 型数值 按位异或 并将结果入栈
0x84
iinc
把指定 int 型增加指定值
Conversions
0x85
i2l
把栈顶 int 强转 long 并入栈
0x86
i2f
把栈顶 int 强转 float 并入栈
0x87
i2d
把栈顶 int 强转 double 并入栈
0x88
l2i
把栈顶 long 强转 int 并入栈
0x89
l2f
把栈顶 long 强转 float 并入栈
0x8A
l2d
把栈顶 long 强转 double 并入栈
0x8B
f2i
把栈顶 float 强转 int 并入栈
0x8C
f2l
把栈顶 float 强转 long 并入栈
0x8D
f2d
把栈顶 float 强转 double 并入栈
0x8E
d2i
把栈顶 double 强转 int 并入栈
0x8F
d2l
把栈顶 double 强转 long 并入栈
0x90
d2f
把栈顶 double 强转 float 并入栈
0x91
i2b
把栈顶 int 强转 byte 并入栈
0x92
i2c
把栈顶 int 强转 char 并入栈
0x93
i2s
把栈顶 int 强转 short 并入栈
Comparisons
0x94
lcmp
比较栈顶两个long 型数值,把结果入栈(-1 or 0 or 1)
0x95
fcmpl
比较栈顶两个 float 型数值,把结果入栈,若有 NaN,入栈 -1
0x96
fcmpg
比较栈顶两个 float 型数值,把结果入栈,若有 NaN,入栈 1
0x97
dcmpl
比较栈顶两个 double 型数值,把结果入栈,若有 NaN,入栈 -1
0x98
dcmpg
比较栈顶两个 double 型数值,把结果入栈,若有 NaN,入栈 -1
0x99
ifeq
当栈顶 int 型数值等于0时,跳转
0x9A
ifne
当栈顶 int 型数值不等于0时,跳转
0x9B
iflt
当栈顶 int 型数值小于0时,跳转
0x9C
ifge
当栈顶 int 型数值大于等于0时,跳转
0x9D
ifgt
当栈顶 int 型数值大于0时,跳转
0x9E
ifle
当栈顶 int 型数值小于等于0时,跳转
0x9F
if_icmpeq
比较栈顶两个 int 型数值,等于0时,跳转
0xA0
if_icmpne
比较栈顶两个 int 型数值,不等于0时,跳转
0xA1
if_icmplt
比较栈顶两个 int 型数值,小于0时,跳转
0xA2
if_icmpge
比较栈顶两个 int 型数值,大于等于0时,跳转
0xA3
if_icmpgt
比较栈顶两个 int 型数值,大于0时,跳转
0xA4
if_icmple
比较栈顶两个 int 型数值,小于等于0时,跳转
0xA5
if_acmpeq
比较栈顶两个 引用 型数值,相等时跳转
0xA6
if_acmpne
比较栈顶两个 引用 型数值,不相等时跳转
Control
0xA7
goto
无条件跳转
0xA8
jsr
跳转指定16bit偏移位置,并将jsr下一条指令地址入栈
0xA9
ret
返回局部变量指定index指定位置,与jsr,jsr_w配合使用
0xAA
tableswitch
switch跳转,case连续
0xAB
lookupswitch
switch跳转,case不连续
0xAC
ireturn
从当前方法返回 int
0xAD
lreturn
从当前方法返回 long
0xAE
freturn
从当前方法返回 float
0xAF
dreturn
从当前方法返回 double
0xB0
areturn
从当前方法返回 对象引用
0xB1
return
从当前方法返回 void
References
0xB2
getstatic
获取类的静态域,并将值入栈顶
0xB3
putstatic
为类的静态域赋值
0xB4
getfield
获取类的实例域,并将值入栈顶
0xB5
putfield
为类的实例域赋值
0xB6
invokevirtual
调用实例方法
0xB7
invokespecial
调用父类构造方法,实例初始化方法,私有方法
0xB8
invokestatic
调用静态方法
0xB9
invokeinterface
调用接口方法
0xBA
invokedynamic
调用动态链接方法
0xBB
new
创建一个对象,并将引用值入栈
0xBC
newarray
创建一个原始类型数组,并将引用值入栈
0xBD
anewarray
创建一个引用类型数组,并将引用值入栈
0xBE
arraylength
获取数组长度并入栈
0xBF
athrow
抛出栈顶异常
0xC0
checkcast
检验类型转换
0xC1
instanceof
检验是否是类的实例,是1入栈,否0入栈
0xC2
monitorenter
获取对象的monitor,用于同步块或方法
0xC3
monitorexit
释放对象的monitor,用于同步块或方法
Extended
0xC4
wide
扩展访问局部变量表的索引宽度
0xC5
multianewarray
创建多维数组,并将引用值入栈
0xC6
ifnull
为 null 时跳转
0xC7
ifnonnull
非 null 时跳转
0xC8
goto_w
无条件跳转(宽索引)
0xC9
jsr_w
跳转指定32bit偏移位置,并将jsr_w下一条指令地址入栈
Reserved
0xCA
breakpoint
调试时的断点
0xFE
impdep1
用于在特定硬件中使用的语言后门
0xFF
impdep2
用于在特定硬件中使用的语言后门

参考:
    ① https://www.cnblogs.com/cwane/p/6097838.html