目录
- JVM指令集
JVM指令集
本文内容基于JVM规范中的第六章和第七章部分, 介绍了JVM中的字节码指令的含义和执行的过程.
一条JVM指令构成:
- 一个操作操作码(opcode), 指定要被处理的操作.
- 零或多个操作数(operand), 体现的是要被操作的值.
无论是操作码还是操作数, 大小都是一个字节.
本文粗略介绍了JVM指令的格式和它对应的操作处理.
值得注意的是, 操作码本身是一个字节的数字, 为了人类可读, 会使用助记符(mnemonic)[其实就是名称]来指代操作码, 后续用助记符来指代操作码的场合将使用斜体.
开始之前简单说一下前置知识:
JVM中的数据类型有 boolean
, byte
, char
, short
, int
, float
, reference
, returnAddress
, long
, double
.
对应的运算类型和分类如下表.
实际类型 | 计算类型 | 分类 |
---|---|---|
boolean | int | 1 |
byte | int | 1 |
char | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
除了数据类型外, 另一个比较重要的概念是栈帧.
方法被调用时产生一个栈帧, 字节码正是在栈帧内被执行的. 栈帧内包含操作数栈(Operand Stack)
, 局部变量表(Local Variables)
, 动态链接(Dynamic Linking)
, 方法出口(Method Invocation Completion)
, 前两者在本文中比较重要.
局部变量表
是一个数组, 其长度在编译时指定, 虚拟机会使用局部变量表传递参数. 表-JVM中的实际类型和计算类型
中分类为1
的类型都使用单个局部变量来储存, long
, double
需要一对局部变量来储存. 实例方法调用中, 局部变量表中的0
位置永远都是this
, 方法参数列表在局部变量表中的索引从1
开始, 类方法调用中, 方法参数列表在局部变量表中的索引从0
开始.
操作数栈
是一个先进后出的栈, 最大栈深度也是在编译时确定的, 操作数栈是字节码执行的临时数据区. 操作数栈
在栈帧被创建时为空, 在执行字节码的过程中, 可以将一些局部变量或常量加载到操作数栈, 或是弹出操作数栈的数据保存到局部变量表等等, 同样, long
, double
将占用两个单位的深度, 而其它类型为一个.
常量操作码(Constants)
常量操作码的范围是0-20. 除了0 nop是什么都不做外, 常量操作码都是将常量压入到操作数栈中. 常量操作码不读取操作数栈的内容.
0 nop
什么也不做.
1 aconst_null
将null
压入操作数栈栈顶. (JVM规范未指定null
的具体值).
2-8 iconst_<i>
将int
类型常量<i>
压入操作数栈. 有 iconst_ml, iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5, 分别代表压入值-1
到5
.
9-10 lconst_<i>
将long
类型常量<i>
压入操作数栈. 有 lconst_0, lconst_1, 分别代表压入值0
和1
.
11-13 fconst_<i>
将float
类型常量<i>
压入操作数栈. 有 fconst_0, fconst_1, fcont_2, 分别代表压入值0
, 1
和2
.
14-15 dconst_<i>
将double
类型常量<i>
压入操作数栈. 有 dconst_0, dconst-1, 分别代表压入值0
和1
.
16 bipush
压入8位整数.
有一个操作数, 将操作数扩展为int
压入操作数栈.
17 sipush
压入16位整数.
有两个操作数, 这两个操作数都是无符号的byte
, 等价于一个short
, 这个short
被拓展为int
压入操作数栈.
18 ldc
加载常量.
有一个操作数, 这个操作数代表常量池中一个可加载常量的索引, 将该可加载的常量压入到操作数栈.
但不能加载 double
, long
型常量, 以及字段描述符是 J
(表示long
)或 D
(表示 double
)的符号引用.
19 ldc_w
加载常量.
ldc 的宽码版本, 有两个操作数, 这两个操作数合并构成一个无符号16位索引, 同样代表常量池中一个可加载的常量, 限制也和 ldc 相同.
20 ldc2_w
加载 double
, long
常量.
有两个操作数, 这两个操作数合并构成一个无符号16位索引, 代表常量池中一个可加载的常量, 但与 ldc 和 ldc_w 的区别是其加载的常量刚好相反, 只加载 double
, long
型常量, 以及字段描述符是 J
(表示long
)或 D
(表示 double
)的符号引用.
加载操作码(Loads)
与常量操作码类似, 加载操作码的结果也是最终在操作数栈压入数据, 不同之处在于, 压入的数据来源通常是局部变量表或数组中的数据而非常量.
21-25 <t>load
这几个指令都有一个操作数, 该操作数代表局部变量表中一个变量的索引, 将该变量压入到操作数栈.
这几个指令是: iload, lload, fload, dload, aload, 分别表示压入一个 int
, long
, float
, double
, reference
类型的变量到操作数栈.
26-45 <t>load_<i>
将局部变量表中索引位置<i>
的<t>
类型的变量压入到操作数栈.
<t>
分别是 i
(表示 int
), l
(表示 long
), f
(表示 float
), d
(表示 double
), a
(表示 reference
)
<i>
分别是 0
, 1
, 2
, 3
, 代表局部变量表中一个变量的索引.
46-53 <t>aload
从操作数栈栈顶弹出 <t>
类型的数组的引用, 再弹出要加载的索引, 将数组中索引位置的数据压入到操作数栈.
<t>
分别是 i
(表示 int
), l
(表示 long
), f
(表示 float
), d
(表示 double
), a
(表示 reference
), b
(表示 byte
), c
(表示 char
), s
(表示 short
).
如果弹出的数组的引用是 null
, 将抛出 NullPointerException
.
如果索引范围越界, 将抛出 ArrayIndexOutOfBoundsException
.
储存字节码(Stores)
储存字节码一般是从操作数栈中读取数组, 并将数据储存到局部变量表或数组中.
54-58 <t>store
有一个操作数, 表示局部变量的索引, 从操作数栈栈顶读取一个对应 <t>
类型的变量储存到局部变量表中, 操作数索引所在位置.
<t>
分别是 i
(表示 int
), l
(表示 long
), f
(表示 float
), d
(表示 double
), a
(表示 reference
), b
(表示 byte
), c
(表示 char
), s
(表示 short
).
59-78 <t>store_<i>
从操作数栈栈顶, 读取一个 <t>
类型的变量, 储存到局部变量表中的 <i>
索引的位置.
<t>
分别是 i
(表示 int
), l
(表示 long
), f
(表示 float
), d
(表示 double
), a
(表示 reference
), b
(表示 byte
), c
(表示 char
), s
(表示 short
).
<i>
分别是 0
, 1
, 2
, 3
.
79-86 <t>astore
从操作数栈弹出<t>
类型的数组的引用, 再从操作数栈弹出 int
类型索引, 最后弹出要进行存储操作的 <t>
类型的值, 将值储存到数组中索引位置.
<t>
分别是 i
(表示 int
), l
(表示 long
), f
(表示 float
), d
(表示 double
), a
(表示 reference
), b
(表示 byte
), c
(表示 char
), s
(表示 short
).
如果数组的引用是null
, 将抛出 NullPointerException
, 如果索引越界, 将抛出 ArrayIndexOutOfBoundsException
.
栈操作码(Stack)
栈操作码, 用于对操作数栈内数据进行操作.
87-88 pop*
弹出操作数栈中的数据. 在弹出一个单位的数据时, 用 pop (87 0x57), 当弹出两个单位的数据时, 使用 pop2 (88 0x58).
如果要弹出的是long
, double
类型, 必须使用 pop2.
89 dup
复制栈顶的数据, 压入栈顶.
\[\dots, value \rightarrow \dots, value, value \]90 dup_x1
复制栈顶的数据, 插入到栈顶的两个单位的操作数的下方.
\[\dots, value2, value1 \rightarrow \dots, value1, value2, value1 \]91 dup_x2
复制栈顶的数据, 插入到栈顶的三个单位的操作数的下方.
\[\dots, value3, value2, value1 \rightarrow \dots, value1, value3, value2, value1 \]value3 和 value2 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(long
和double
).
92 dup2
复制操作数栈栈顶的两个数据, 压入栈顶.
\[\dots, value2, value1 \rightarrow \dots, value2, value1, value2, value1 \]value2 和 value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(long
和double
).
93 dup2_x1
复制操作数栈栈顶的两个数据, 插入栈顶三个单位操作数之下.
\[\dots, value3, value2, value1 \rightarrow \dots, value2, value1, value3, value2, value1 \]value2 和 value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(long
和double
).
value3 只能是占用一个单位空间的类型.
94 dup2_x2
复制操作数栈栈顶的两个操作数, 插入栈顶四个单位操作数之下.
\[\dots, value4, value3, value2, value1 \rightarrow \dots, value2, value1, value4, value3, value2, value1 \]value2 和 value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(long
和double
).
value4 和 value3 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(long
和double
).
95 swap
交换栈顶两个操作数.
\[\dots, value2, value1 \rightarrow \ ..., value1, value2 \]value2 和 value1 的类型必须是分类为1
的类型.
JVM不提供分类为2
的类型的交换指令.
数学操作码(Math)
数学运算的操作码.
96-99 <t>add
加.
<t>
可以是 i
, l
, f
, d
.
将操作数数栈栈顶的两个操作数相加, 结果压入操作数栈.
参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.
100-103 <t>sub
减.
<t>
可以是 i
, l
, f
, d
.
将操作数数栈栈顶的两个操作数相减, 结果压入操作数栈. 栈顶的是被减的数.
参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.
104-107 <t>mul
乘.
<t>
可以是 i
, l
, f
, d
.
将操作数数栈栈顶的两个操作数乘, 结果压入操作数栈.
参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.
108-111 <t>div
除.
<t>
可以是 i
, l
, f
, d
.
将操作数数栈栈顶的两个操作数相除, 结果压入操作数栈. 栈顶的是除数.
参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.
112-115 <t>rem
取余.
<t>
可以是 i
, l
, f
, d
.
将操作数数栈栈顶的两个操作数取余, 结果压入操作数栈.
当为int
和long
时, 栈顶的两个值 value2 和 value1 (value2 先弹出) 的运算规则是 result = value1 - (value1 / value2) * value2.
当为float
和dobule
时, 运算规则为IEEE754
标准.
116-119 <t>neg
取反.
<t>
可以是 i
, l
, f
, d
.
弹出操作数栈栈顶的值 value , 取反后压入操作数栈.
对于 int
和 long
, 设 x
为 value, -x
等价于 (~x) + 1
(按位取反 + 1), 并且因为负数比正数多, 负数最小值取负数得到的是自身.
对于 float
和 double
, 运算规则为IEEE754
标准:
- 如果 value 是
NaN
, 结果是NaN
- 如果 value 是无穷, 结果是符号位相反的无穷
- 如果 value 是
0
, 结果是符号位相反的0
.
120-121 <t>shl
左移.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 将 value1 左移 s 位作为结果压入操作数栈, 其中 s 是 value2 低5位的值.
相当于是 value1 乘以 2 的 s 次方(就算会溢出), s 的范围是 0 - 31 之间, 等同于 value 与 0x1f
按位与的结果.
122-123 <t>shr
右移.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 将 value1 右移 s 位作为结果压入操作数栈, 其中 s 是 value2 低5位的值.
右移时, 在高位补充符号位.
相当于是 value1 除以 2 的 s 次方, s 的范围是 0 - 31 之间, 等同于 value 与 0x1f
按位与的结果.
124-125 <t>ushr
无符号右移.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 将 value1 右移 s 位作为结果压入操作数栈, 其中 s 是 value2 低5位的值, 即s 为 value1 & 0x1f, .
右移时, 在高位补充0
.
如果 value1 为正数, 结果与 value1 >> s 相同; 如果 value1 为负数, 结果等于 (value1 >> s) + (2 << ~s), (2 << ~s) 抵消了增加的符号位.
s 的范围是 0 - 31 之间.
126-127 <t>and
按位与.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 进行按位与运算后将结果压入操作数栈.
128-129 <t>or
按位或.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 进行按位或运算后将结果压入操作数栈.
130-131 <t>xor
异或.
<t>
可以是 i
或 l
.
弹出操作数栈栈顶的 value2 和 value1 (value2 先弹出), 进行按位异或运算后将结果压入操作数栈.
132 iinc
自增.
操作码后是一个字节的无符号整数 index 指向局部变量表中 int
类型的变量, 以及一个字节的有符号整数 const. 将局部变量表中索引位置的变量增加 const.
iinc 可以通过 wide 字节码拓展, 以访问局部变量表中两字节索引位置的变量, 或增加两字节的有符号立即数.
转换操作码(Conversions)
133-147 <t1>2<t2>
弹出操作数栈栈顶的<t1>
类型的操作数, 强转为<t2>
类型, 压入操作数栈, 在向下转型的时候, 可能会发生丢失精度.
操作数列表:
i2l = 133 (0x85)
i2f = 134 (0x86)
i2d = 135 (0x87)
l2i = 136 (0x88)
l2f = 137 (0x89)
l2d = 138 (0x8a)
f2i = 139 (0x8b)
f2l = 140 (0x8c)
f2d = 141 (0x8d)
d2i = 142 (0x8e)
d2l = 143 (0x8f)
d2f = 144 (0x90)
i2b = 145 (0x91)
i2c = 146 (0x92)
i2s = 147 (0x93)
比较操作码(Comparisons)
比较操作数栈的值, 产生结果或进行跳转.
148 lcmp
比较 long
类型.
比较操作数栈栈顶的两个long
型操作数, 结果为int
类型, 压入到操作数栈栈顶. 栈顶的操作数分别为 value1, value2, ..., 如果 value1 大于 value2, 结果为 1
, 如果 value1 等于 value2, 结果为0
, 如果 value1 小于 value2, 结果为 -1
.
149-152 <t>cmp<op>
浮点数的比较. <t>
是 f
或d
, <op>
是g
或l
, <op>
代表的是对NaN
的处理方式.
fcmpl = 149 (0x95)
fcmpg = 150 (0x96)
dcmpl = 151 (0x97)
dcmpg = 152 (0x98)
这些指令比较操作数栈栈顶的两个浮点数, 比较的结果以int
类型压入操作数栈栈顶. 栈顶的操作数分别为 value1, value2, ..., 如果 value1 大于 value2, 结果为 1
, 如果 value1 等于 value2, 结果为0
, 如果 value1 小于 value2, 结果为 -1
. 当 value1 或 value2 是 NaN
的情况下, fcmpg 和 dcmpg 将 1
压入操作数栈, fcmpl 和 dcmpl 将 -1
压入操作数栈.
153-158 if<cond>
ifeq = 153 (0x99)
ifne = 154 (0x9a)
iflt = 155 (0x9b)
ifge = 156 (0x9c)
ifgt = 157 (0x9d)
ifle = 158 (0x9e)
指令中有两个操作数, 弹出操作数栈栈顶的int
类型的操作数, 比较成功之后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为 if<cond> 操作码的地址.
若操作数栈弹出的操作数为 value, 则:
- ifeq 只在 value = 0 时成功
- ifne 只在 value ≠ 0 时成功
- iflt 只在 value < 0 时成功
- ifge 只在 value ≥ 0 时成功
- ifgt 只在 value > 0 时成功
- ifle 只在 value ≤ 0 时成功
比较不成功时, 继续执行 if<cond> 后面的操作码.
159-164 if_icmp<cond>
指令中有两个操作数, 弹出操作数栈栈顶的两个int
类型的操作数, 比较成功后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为if_icmp<cond>操作码的地址.
if_icmpeq = 159 (0x9f)
if_icmpne = 160 (0xa0)
if_icmplt = 161 (0xa1)
if_icmpge = 162 (0xa2)
if_icmpgt = 163 (0xa3)
if_icmple = 164 (0xa4)
若操作数栈弹出的操作数分别为 value2, value1 (value2 先弹出), 则:
- if_icmpeq 只在 value1 = value2 时成功
- if_icmpne 只在 value1 ≠ value2 时成功
- if_icmplt 只在 value1 < value2 时成功
- if_icmpge 只在 value1 ≥ value2 时成功
- if_icmpgt 只在 value1 > value2 时成功
- if_icmple 只在 value1 ≤ value2 时成功
比较不成功时, 继续执行 if_icmp<cond> 后面的操作码.
165-166 if_acmp<cond>
指令中有两个操作数, 弹出操作数栈栈顶的两个reference
类型的操作数, 比较成功后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为if_acmp<cond> 操作码的地址.
if_acmpeq = 165 (0xa5)
if_acmpne = 166 (0xa6)
若操作数栈弹出的操作数分别为 value1, value2 (value1 先弹出), 则:
- if_acmpeq 只在 value1 = value2 时成功
- if_acmpne 只在 value1 ≠ value2 时成功
比较不成功时, 继续执行 if_acmp<cond> 后面的操作码.
控制操作码(Control)
流程控制的操作码.
167 goto
直接跳转.
有两个操作数, 跳转到两个操作数组成的16位数字偏移量的地址, 偏移起始地址从 goto 操作码开始.
168 jsr
跳转异常处理的子过程.
class
文件版本低于50(对应jdk6)或以下, 才会使用 jsr. 更高版本的jdk中已经不再使用.
jsr 用于进行 try-finally
语句的跳转控制.
jsr 有两个操作数, 组成了一个16位数字偏移, 偏移相对起始地址为 jsr 操作码的地址, JVM将跳转到指定的偏移地址上, 并且将 jsr 后面的操作码的地址以 returnAddress
类型压入到操作数栈.
169 ret
从异常处理的子过程中返回.
class
文件版本低于50(对应jdk6)或以下, 才会使用 ret. 更高版本的jdk中已经不再使用.
ret 用于进行 try-finally
语句的跳转控制.
有一个操作数, 这个操作数是一个0-255之间的无符号整数, 代表当前栈帧中的局部变量表的一个包含了returnAddress
类型的索引, 程序将跳转该地址, 并继续执行.
170 tableswitch
通过表索引进行跳转.
tableswitch 指令的格式: tableswitch 操作码; 0-3 字节的填充; 4字节的数字表示default
语句跳转的偏移量: default offset; 4字节的最小的匹配case
: low; 4字节的最大的匹配case
: high; 最大case
- 最小case
+ 1个 4字节数字表示对应case
跳转的偏移量: jump offsets;
偏移量都是相对于 tableswitch 操作码的地址.
执行时弹出操作数栈栈顶的int
类型数据 index, 如果不在匹配的范围中, 跳转到default
偏移的地址, 否则, 通过index - low 作为索引在 jump offsets中获取对应的偏移量, 跳转偏移对应的地址继续执行.
填充的字节数由 tableswitch 离方法的开始(方法内的第一个字节)的距离决定. 在填充后, default offset 的第一个字节的开始距离方法开始的字节数必须是4的整倍数.
例如: tableswitch 是方法内的第10个字节(前面的字节码总计占据9个字节), 那么就需要填充两个字节, 如果是第12个字节, 则无需填充.
171 lookupswitch
通过查找比较进行跳转.
lookupswitch 指令的格式: lookupswitch 操作码; 0-3 字节的填充; 4字节的数字表示default
语句跳转的偏移量: default offset; 4字节的数字表示有多少对match-offset: npairs; 长度为 npairs 的 match-offset pairs, 每对 match-offset 为8字节, 前4字节的数字表示匹配的case
, 后4字节为对应case
的偏移量, match-offset pairs 的case
排序必须是递增的.
偏移量都是相对于 lookupswitch 操作码的地址.
执行时弹出操作数栈栈顶的int
类型数据 index, 在 match-offset pairs 中查找匹配的case
, 找到后跳转匹配的case
的偏移量继续执行, 没有找到这跳转 default offset 的偏移量继续执行.
填充的字节数由 lookupswitch 离方法的开始(方法内的第一个字节)的距离决定. 在填充后, default offset 的第一个字节的开始距离方法开始的字节数必须是4的整倍数.
例如: lookupswitch 是方法内的第10个字节(前面的字节码总计占据9个字节), 那么就需要填充两个字节, 如果是第12个字节, 则无需填充.
172-177 <t>return
返回 <t>
类型的结果.
ireturn = 172 (0xac)
lreturn = 173 (0xad)
freturn = 174 (0xae)
dreturn = 175 (0xaf)
areturn = 176 (0xb0)
return = 177 (0xb1)
返回操作数栈栈顶的<t>
类型. <t>
按顺序是: i
, l
, f
, d
, a
, 以及一个返回 void
类型的 return 指令.
如果方法有 synchronized
修饰, 则会释放锁. 但是如果未持有锁则抛出IllegalMonitorStateException
.
在JVM实现未遵循JVM规范中对于锁的规范时, 可能会发生这种进入了 synchronized
方法但却不持有锁的情况.
引用操作码(References)
通过引用来进行一些操作的操作码, 大部分引用操作码都是与面向对象
178 getstatic
读取 static
字段.
有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 与 ldc 的区别是, getstatic 用于读取常量池中索引位置的static
字段的符号引用压入操作数栈.
成功解析字段后, 字段所属的类或接口还未初始化, 则该类或接口将初始化.
179 putstatic
设置 static
字段.
有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 将操作数栈栈顶的操作数弹出, 赋值给常量池中索引位置的字段.
如果字段的类型是boolean
, byte
, short
, int
, char
, 栈顶的操作数必须为int
类型, 如果为float
, long
或 double
则必须为对应的float
, long
, double
类型. 如果字段是引用类型, 则必须符合赋值兼容性.
180 getfield
读取对象字段.
有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 索引位置是字段的符号引用, 该引用的字段已经解析.
弹出栈顶的 objectref, objectref 是reference
类型但不是数组类型, 弹出后, 获取引用对象的字段数据并压入操作数栈.
链接时: 可能抛出字段引用解析的异常;如果字段是static
的, 将抛出IncompatibleClassChangeError
异常.
运行时: 如果objectref 是 null
, 将抛出NullPointerException
.
getfield 不能访问数组的
length
字段, 而是通过arraylength
字节码获取数组长度.
181 putfield
设置对象字段.
有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 索引位置是字段的符号引用, 该引用的字段已经解析.
弹出栈顶的 value 和 objectref (value 在最上面), objectref 是 reference
类型, 但不是数组类型, 将 value 赋值给 objectref 的字段.
value 的类型必须与字段匹配
- 如果字段描述符类型是
boolean
,byte
,char
,short
或int
, value 的类型必须是int
. - 如果字段描述符类型是
float
,long
,double
, value的类型必须也对应地是float
,long
,double
. - 如果字段描述符类型是引用类型, value 的类型必须满足赋值兼容性规范.
- 如果字段是
final
的, 该指令必须是在当前类的构造方法中.
value 是 int
类型而字段是 boolean
类型时, 被设置的值是value & 1 的结果.
182 invokevirtual
调用实例方法, 基于类进行分派.
调用类方法. 有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法引用. 该方法引用已被解析.
弹出栈顶的 nargs 和 objectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectref 是 reference
类型, 但不是数组类型.
针对类型为 C 的 objectref 和常量池中被解析的方法 \(m_R\) , 选择方法调用.
对于一般方法, 选择方法的流程是:
-
如果 \(m_R\) 被
ACC_PRIVATE
标记, 该方法被选中. -
否则, 通过以下步骤查找方法
-
如果 C 中声明了实例方法 m, 该方法可以重写方法 \(m_R\), 方法 m 被选中.
-
否则, 从 C 的直接父类中开始, 按继承结构从下往上查找可以重写 \(m_R\) 的方法,
-
否则, 从 C 的超接口中查找名称和描述符一致的方法.
https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-5.html#jvms-5.4.6
-
还有一些其它情况这里水平有限不多说了, 参考原文: https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-6.html#jvms-6.5.invokevirtual
按<<深入Java虚拟机中>>对于 invokevirtual 的解释是:
invokevirtual 用于调用所有虚方法.
invokevirtual 指令的运行时解析过程大致分为以下几步:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果 通过则返回这个方法的直接引用,查找过程结束;不通过则返回
java.lang.IllegalAccessError
异常。- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出
java.lang.AbstractMethodError
异常。笔者注:
当存在参数时, 第一步找到调用方法的调用者不是在栈顶.
例如下面这这段代码:
public void m(){ System.out.println("hello"); }
m
方法对应的字节码0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String hello 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
可以看到, getstatic 指定先将
System.out
压入栈, 再通过 ldc 从常量池加载"hello"
, 从栈顶往下, 依次是参数列表的值(从右往左), 和调用者.读者可以尝试调用多个参数的方法, 验证参数入栈的顺序.
另外, 下面的
B.m
方法与A.m
方法具有相同的名称和描述符, 并且A.call
方法中的a
的实际类型也是B
, 但实际调用的方法仍然是A.m
.package a; public class A { void m() { System.out.println("A.m()"); } public void call(A a) { a.m(); } }
package b; public class B extends A { void m() { System.out.println("B.m()"); } public static void main(String[] args) { var b = new B(); b.call(b); } }
输出是
A.m()
另外还有一个小
trick
如下:在包
pa
中有如下类:package pa; public class A { void m() { System.out.println("A.m()"); } public void call(A a) { a.m(); } }
package pa; public class B extends A { void m() { System.out.println("B.m()"); } public void call(B b) { b.m(); } }
package pa; public class C extends B { void m() { System.out.println("C.m()"); } public void call(C c) { c.m(); } }
在另一个包
pb
中有:package pb; public class D extends C { void m() { System.out.println("D.m()"); } public static void main(String[] args) { var d = new D(); d.call((A) d); d.call((B) d); d.call((C) d); } }
通过
javac -cp . pb\D.java
再java -cp . pb.D
运行的输出是:C.m() C.m() C.m()
对此的解释是:
A
,B
,C
在一个包中, 并有继承关系, 对于package-private
访问级别的m
方法,B
重写了A
,C
重写了B
.但是
D
在另一个包中,D
的m
方法并没有重写C
, 因为package-private
对于另一个包不可见.因此通过
D
的实例调用A.m
,B.m
,C.m
, 都将调用到C.m
.如果将
B
类的m
方法改为public
的, 即:package pa; public class B extends A { public void m() { System.out.println("B.m()"); } public void call(B b) { b.m(); } }
此时将无法直接正确编译
C
和D
的, 因为子类方法的访问等级不能更低.但可以手动地单独编译
B
类, 即javac -cp pa\B.java
, 因为C
与D
已经编译过了, 所以再一次运行D
, 输出是:D.m() D.m() C.m()
对此的解释是:
B
的m
方法为public
之后, 根据JVM规范对于重写的定义, 此时D
重写了B
的m
方法, 但没有重写C
, 并且, 因为重写传递性,A
的m
方法也被重写.
183 invokespecial
调用实例方法, 调用构造方法或当前类方法或父类方法.
有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法的符号引用. 该方法已被解析.
操作数栈必须包含 nargs 和 objectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectref 是 reference
类型, 但不是数组类型.
如果满足以下所有条件, 定义 C 为当前类的直接父类.
- 被解析的方法不是实例初始化方法
- 方法的符号引用中, 类的符号引用是当前类的父类
- 当前类包含
ACC_SUPER
标记
否则, C 是方法的符号引用的类或接口.
实际被调用的方法的选择流程如下:
- C 中声明了与方法引用名称和描述符相同的方法, 该方法将被调用
- 否则, 如果 C 是一个类并且有父类, 从 C 的直接父类中开始, 按继承结构从下往上查找符合步骤1的方法, 直至没有父类或找到方法.
- 否则, 如果 C 是一个接口, 并且方法引用的名称和描述符与
Object
中声明的public
方法一致, 则调用该方法. - 否则, 从 C 的超接口中查找名称和描述符一致的方法.
invokespecial 和 invokevirtual 的最大区别是, 调用者的实际类型不会影响 invokespecial 进行方法查找的逻辑, 而 invokevirtual 必须依赖调用者的实际类型进行查找.
invokespecial 在查找方法时也不关注方法的重写关系.
另外对于构造方法 <init>
, 必须使用 invokespecial 调用.
在java中, 对于 构造方法(<init>
), super
方法(类或接口的super
), private
方法, 都将使用 invokespecial 调用.
184 invokestatic
有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法的符号引用. 该方法已被解析.
该方法不能是实例方法, 或类和接口的初始化方法.
该方法必须是 static
, 并且不能被 abstract
修饰.
成功解析方法后,如果类或接口尚未初始化,则声明已解析方法的类或接口将被初始化.
操作数栈必须包含 nargs, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致.
185 invokeinterface
调用接口方法. 有4个操作数, 前两个操作数构成一个16位的无符号数字的索引, 指向常量池中一个方法引用. 该方法引用已被解析.
第三个操作数记为 count, count 是无符号byte
类型, 并且不能为0. 最后一个操作数恒为0.
操作数栈必须包含 nargs 和 objectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectref 是 reference
类型, 但不是数组类型.
其方法选择的逻辑与 invokevirtual 相同.
调用时其它处理参考原文: https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-6.html#jvms-6.5.invokeinterface
186 invokedynamic
调用动态调用点.
有四个操作数, 前两个操作数构成一个16位的无符号数字的索引, 指向常量池中一个动态调用点的符号引用, 后两个操作数目前恒为0.
动态调用点的符号引用由指向引导方法表的索引和指向常量池中的名称和描述符的索引构成.
每个动态调用掉绑定一个引导方法, 在动态调用点解析的时候会调用引导方法, 将得到 java.lang.invoke.CallSite
实例的引用. java.lang.invoke.CallSite
的实例被认为是 "绑定" 这个特定的 invokedynamic 指令.
invokedynamic 执行调用点, 从操作数栈中弹出 nargs, nargs 是参数, 与动态调用点的描述符的参数列表数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)一致, 调用结束后产生动态调用描述符的返回类型的引用, 压入操作数栈.
例如:
public static void main(String[] args) {
Runnable runnable = () -> System.out.println();
}
其字节码信息如下(省略部分其它字节码):
Constant pool:
#7 = InvokeDynamic #0:#8 // #0:run:()Ljava/lang/Runnable;
#8 = NameAndType #9:#10 // run:()Ljava/lang/Runnable;
#9 = Utf8 run
#10 = Utf8 ()Ljava/lang/Runnable;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #7, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: return
BootstrapMethods:
0: #42 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#49 ()V
#50 REF_invokeStatic moe/wymc/a/A.lambda$main$0:()V
#49 ()V
可以看到main
方法中, invokedynamic 指令调用的动态调用点 #7
, 在常量池中是一个动态调用点, 其组成是 #0
指向引导方法表, #8
则是动态调用点的名称和描述符.
稍微修改一下代码:
public static void main(String[] args) {
int i = 0;
Runnable runnable = () -> System.out.println(i);
}
Constant pool:
#7 = InvokeDynamic #0:#8 // #0:run:(I)Ljava/lang/Runnable;
#8 = NameAndType #9:#10 // run:(I)Ljava/lang/Runnable;
#9 = Utf8 run
#10 = Utf8 (I)Ljava/lang/Runnable;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: invokedynamic #7, 0 // InvokeDynamic #0:run:(I)Ljava/lang/Runnable;
8: astore_2
9: return
BootstrapMethods:
0: #42 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#49 ()V
#50 REF_invokeStatic moe/wymc/a/A.lambda$main$0:(I)V
#49 ()V
注意观察常量池中 #10
, 即动态调用的描述符的变化, 其参数列表多了捕获的变量 i
的类型 int
, 其表示为 I
, 相应的, 在 main
方法的调用栈中也出现了局部变量的存取, 在 invokedynamic 调用时, 操作数栈中有一个 int
类型的操作数 1
.
读者可以多尝试用 lambda
来实现不同的 FunctionInterface
, 观察其字节码.
JVM中规定引导方法的解析必须是在运行时, 而非在类加载过程中解析, 即 invokedynamic 首次执行时才会执行绑定的引导方法.
187 new
创建新对象, 压入该对象的引用. 有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个类或接口类型的符号引用. 该符号引用已解析, 并且应该产生一个类的类型.
该类的新实例的内存从堆中分配,新对象的实例变量被初始化为其默认的初始值, 指令完成后, 类型为 reference 的操作数 objectref 被压入操作数栈.
当类解析成功时, 如果类还没初始化将进行初始化.
如果符号引用解析的结果是接口或抽象类, 将抛出InstantiationError
.
new 指令不会完整的创建对象, 直到调用类的初始方法.
188 newarray
创建新的基础类型的数组, 压入该数组的引用. 有一个操作数 atype, atype 有 8 个值, 代表要初始化的8种不同的数组类型.
Array Type | atype |
---|---|
T_BOOLEAN | 4 |
T_CHAR | 5 |
T_FLOAT | 6 |
T_DOUBLE | 7 |
T_BYTE | 8 |
T_SHORT | 9 |
T_INT | 10 |
T_LONG | 11 |
操作数栈栈顶为int
类型的 count, 表示要创建的数组的长度.
在堆上分配长度为 count , 元素类型为 atype 的数组, 其引用压入到操作数栈中, 数组的每个元素的值被初始化为默认值.
在Oracle的JVM上, 对应 boolean
类型的数组使用的是8位值的数组. 其它虚拟机可能不一样, 但仍然必须使用 baload 和 bastore 来访问这些数组.
189 anewarray
创建新的引用类型的数组, 压入该数组的引用.
有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.
弹出操作数栈栈顶为 int
类型的 count, 表示要创建的数组的长度.
在堆上分配长度为 count 的对应类型的数组, 其引用压入到操作数栈中, 数组的每个元素的值被初始化为默认值 null
.
anewarray 只能用于创建一维数组.
190 arraylength
获取数组长度.
操作数栈栈顶必须是数组的引用, 将其长度压入到操作数栈.
如果引用为 null
, 将抛出空指针异常.
191 athrow
抛出异常.
操作数栈栈顶是 Throwable
类型或其子类型的实例的引用 objectref, 弹出 objectref , 然后通过在当前方法的异常表中查找第一个与 objectref 类型匹配的异常处理器, 异常表中包含处理器的跳转地址, 当前栈帧的程序计数器被设置为跳转的地址, 操作数栈被清空, 并压入异常对象的 objectref, 程序从跳转地址继续执行.
如果找不到异常处理器, 从虚拟机栈中弹出当前栈帧, 恢复调用者的栈帧, 并重新抛出异常, 如果不再存在栈帧, 则退出线程.
192 checkcast
检查对象是否是指定的类型.
有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.
checkcast 不改变操作数栈, 只对栈顶对象引用进行类型检查, 如果栈顶元素是 null
, 或栈顶元素可以转换成已解析的符号引用的类型, 则什么都不变, 否则, 抛出ClassCastException
.
判断栈顶元素 objectref 非空情况下, 其类型为 S, 已解析的符号引用的类型是 T, 判断 objectref 能转型为 T 的规则如下:
- 如果 S 是类
- 如果 T 是类, S 必须是 T 或者 T 的子类
- 如果 T 是接口, S 必须实现接口 T
- 如果 S 是数组类型 SC[], 即其元素类型是 SC
- 如果 T 是类, T 必须是
Object
类 - 如果 T 是接口, T 必须是
Cloneable
或 Serializable - 如果 T 是数组类型 TC[], 即元素类型是 TC, 满足下列条件之一即可
- TC 和 SC 是相同的原始数据类型
- TC 和 SC 是引用类型,并且可以通过递归应用这些规则将类型 SC 强制转换为 TC
- 如果 T 是类, T 必须是
checkcast 并不会改变引用的实际类型, 在 checkcast 成功时也不会修改操作数栈, 只会在失败时抛出 ClassCastException
.
checkcast 和 instanceof 非常相似, 不同点在于: 1. 对 null
的处理不同; 2. 失败时的处理不同; 3. 对操作数栈的影响不同;
193 instanceof
确定对象是否是指定的类型.
有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.
弹出操作数栈栈顶的引用类型的 objectref, 检查其类型是否可以转换为已解析的符号引用的类型.
如果 objectref 是 null
, 将 int
类型的 0
压入操作数栈, 如果可以转型则将 int
类型的 1
压入操作数栈, 否则, 压入 int
类型的 0
到操作数栈.
instanceof 检查是否可以转型的条件与 checkcast 一致, 区别在于: 1. 对于 null
, instanceof 视为不能转型, 压入 0
到操作数栈, 但 checkcast 视为可以转型; 2. instanceof 会弹出检查的引用类型, 并将结果压入操作数栈;
194 monitorenter
进入对象的监视器.
操作数栈栈顶是类型为 reference
类型的 objectref. 每个对象都被分配了一个监视器, 当且仅当监视器有所有者时将被锁定.
当线程执行 monitorenter 时, 弹出操作数栈栈顶的 objectref, 尝试获取分配给 objectref 的 监视器的所有权:
- 如果 objectref 关联的监视器的计数为0, 当前线程进入监视器, 并设置其计数为1, 当前线程成为监视器是所有者.
- 如果当前线程已经取得了 objectref 关联的监视器的所有权, 则重入监视器, 并增加其计数.
- 如果另一个线程已经取得了 objectref 关联的监视器的所有权, 当前线程会阻塞, 直到监视器的计数为0, 再尝试获取其所有权
195 monitorexit
退出对象的监视器.
弹出操作数栈栈顶是类型为 reference
类型的 objectref, 执行 monitorexit 指令的线程必须是 objectref 的监视器的所有者, 减少监视器计数, 如果计数为0, 当前线程不再是 objectref 的监视器的所有者, 而其它被阻塞的线程可以进入监视器.
如果当前线程不是监视器的所有者, 则抛出 IllegalMonitorStateException
.
monitorenter 和 monitorexit 一起使用可以实现 synchronized
代码块, 但不用于实现 synchronized
方法, 尽管他们语义上是等价的.
拓展操作码(Extended)
196 wide
使用额外的字节拓展局部变量表的索引.
wide 操作码用于修改另一个操作码的行为, 取决于被修改的字节码, 有两种格式.
第一种格式用于修改字节码 iload, fload, aload, lload, dload, istore, fstore, astore, lstore, dstore, 或 ret.
另一种格式仅用于修改 iinc.
无论哪种情况, wide 操作码后面紧跟着的都是被修改的操作码, 随后是两个字节表示无符号16位数字索引, 指向当前栈帧的局部变量表.
当 wide 修改的是 lload, dload, lstore, dstore时, 索引加一的也仍然局部变量表的索引.
在第二种格式中, 紧跟着局部变量表索引后的是两个字节表示的有符号16位数字常量.
wide 指令实际上是将被修改的指令视为操作数, 从而改变指令的性质, 其嵌入的指令不能直接运行, 也不能成为控制跳转的目标.
197 multianewarray
创建多维数组.
操作码后有两个字节的立即数构成一个无符号的16位数索引指向常量池中一个类, 数组或接口类型的符号引用.
随后是一个字节的无符号数, 表示要创建的数组的维度, 必须大于等于1.
运行时, 符号引用已解析, 解析的结果必须一个数组类的类型, 其维度大于或等于指令中的维度立即数.
操作数栈栈顶依次是各个维度的数组的长度, 其入栈顺序与维度增长顺序相同, 例如:
new String[1][2];
javap -v
显示字节码为:
0: iconst_1
1: iconst_2
2: multianewarray #25, 2 // class "[[Ljava/lang/String;"
一个新的多维数组将在堆上被创建, 如果其中有数组的长度为0, 其子维度数组将不再分配.
每个维度的元素都是其子维度的元素的数组类型, 最后一个维度的元素被初始化为对应的类型的默认值, 其它维度被初始化为 null
.
如果被创建的数组只有一个维度, 通常使用 newarray 或 anewarray 更有效.
下面这段代码是常量池中的数组类型的维度大于 multianewarray 指令中维度的一个例子.
var array = new String[1][2][];
0: iconst_1
1: iconst_2
2: multianewarray #25, 2 // class "[[[Ljava/lang/String;"
在这种情况下, 没有长度的维度将不会被创建, 对应的, 如果只创建了一维, 尽管类型是更多维度的, 那将会使用 newarray 或 anewarray 指令.
var array = new String[1][];
0: iconst_1
2: anewarray # 25 // class "[Ljava/lang/String;"
注意 multianewarray
指向的常量池中的类型是创建数组对象的类型, 而 newarray 或 anewarray 指向的常量池中的类型是数组元素的类型.
198 ifnull
引用为 null
的分支.
操作码后有两个字节构成的16为无符号数字代表的偏移量.
弹出操作数栈栈顶元素 value, 其类型为 reference
, 如果 value 是 null
, 则跳转相对于 ifnull 指令位置的偏移量之后继续执行, 否则, 继续执行后续的执行.
199 ifnonnull
引用不为 null
的分支.
操作码后有两个字节构成的16为无符号数字代表的偏移量.
弹出操作数栈栈顶元素 value, 其类型为 reference
, 如果 value 不是 null
, 则跳转相对于 ifnonnull 指令位置的偏移量之后继续执行, 否则, 继续执行后续的执行.
200 goto_w
跳转一定偏移量.
操作码后有四个字节构成的32位整数构成的偏移量, 跳转相对于 goto_w 地址的偏移量的位置继续执行.
与 goto 唯一的不同是跳转的偏移量使用四个字节而不是两个字节表示.
201 jsr_w
class
文件版本低于50(对应jdk6)或以下, 才会使用 jsr_w. 更高版本的jdk中已经不再使用.
处理同 jwr, 唯一区别是跟随操作码后的是四个字节的32位的偏移量, 而 jsr 是两个字节.
保留操作码(Reserved Opcodes)
JVM规范中定义了三个保留操作码, 供JVM内部实现, 将来JVM指令集拓展也不会使用保留操作码.
合法的class
文件中不应该出现这些操作码, 但与已经加载的JVM交互的调试器或JIT代码生成器等工具可能会碰到这些指令.
202 breakpoint
breakpoint 202 (0xca), 用于debugger实现断点.
254 impdep<i>
impdep1 (0xfe), impdep2 (0xff), 用于在软硬件特定功能实现提供"后门"或 traps.
标签:操作数,指令集,压入,JVM,操作码,value2,value1,类型 From: https://www.cnblogs.com/wymc/p/17513929.html