【Java 虚拟机笔记】字节码指令相关整理
2019-03-11 本文已影响29人
58bc06151329
文前说明
作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
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 运行时所有的数据类型都有对应与之相关的指令去支持的话,操作码的总数将超过 256 条。所以 Java 字节码指令集被设计为 Not Orthogonal(非完全独立),即并非每种数据类型和每种操作都有对应的指令,一些指令可以在必要的时候将一些不被支持的数据类型转换为被支持的数据类型。
- 当数据大小超过一个字节时,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。
- 函数的传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
指令码 | 助记符 | 说明 |
---|---|---|
0x15 | iload | 将指定的 int 型本地变量推送至栈顶。 |
0x16 | lload | 将指定的 long 型本地变量推送至栈顶。 |
0x17 | fload | 将指定的 float 型本地变量推送至栈顶。 |
0x18 | dload | 将指定的 double 型本地变量推送至栈顶。 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶。 |
0x1a | iload_0 | 将第一个 int 型本地变量推送至栈顶。 |
0x1b | iload_1 | 将第二个 int 型本地变量推送至栈顶。 |
0x1c | iload_2 | 将第三个 int 型本地变量推送至栈顶。 |
0x1d | iload_3 | 将第四个 int 型本地变量推送至栈顶。 |
0x1e | lload_0 | 将第一个 long 型本地变量推送至栈顶。 |
0x1f | lload_1 | 将第二个 long 型本地变量推送至栈顶。 |
0x20 | lload_2 | 将第三个 long 型本地变量推送至栈顶。 |
0x21 | lload_3 | 将第四个 long 型本地变量推送至栈顶。 |
0x22 | fload_0 | 将第一个 float 型本地变量推送至栈顶。 |
0x23 | fload_1 | 将第二个 float 型本地变量推送至栈顶。 |
0x24 | fload_2 | 将第三个 float 型本地变量推送至栈顶。 |
0x25 | fload_3 | 将第四个 float 型本地变量推送至栈顶。 |
0x26 | dload_0 | 将第一个 double 型本地变量推送至栈顶。 |
0x27 | dload_1 | 将第二个 double 型本地变量推送至栈顶。 |
0x28 | dload_2 | 将第三个 double 型本地变量推送至栈顶。 |
0x29 | dload_3 | 将第四个 double 型本地变量推送至栈顶。 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶。 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶。 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶。 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶。 |
store 系列指令
- 把栈顶的值存入本地变量。这里的本地变量不仅可以是数值类型,还可以是引用类型。
- 对于前四个本地变量可以采用 istore_0、istore_1、istore_2、istore_3(分别表示第 0、1、2、3 个整形变量)这种无参简化命令形式。
- 对本地变量所进行的编号,是对所有类型的本地变量进行的(并不按照类型分类)。
- 对于非静态函数,第一变量是 this,它是只读的。
- 函数传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
指令码 | 助记符 | 说明 |
---|---|---|
0x36 | istore | 将栈顶 int 型数值存入指定本地变量。 |
0x37 | lstore | 将栈顶 long 型数值存入指定本地变量。 |
0x38 | fstore | 将栈顶 float 型数值存入指定本地变量。 |
0x39 | dstore | 将栈顶 double 型数值存入指定本地变量。 |
0x3a | astore | 将栈顶引用型数值存入指定本地变量。 |
0x3b | istore_0 | 将栈顶 int 型数值存入第一个本地变量。 |
0x3c | istore_1 | 将栈顶 int 型数值存入第二个本地变量。 |
0x3d | istore_2 | 将栈顶 int 型数值存入第三个本地变量。 |
0x3e | istore_3 | 将栈顶 int 型数值存入第四个本地变量。 |
0x3f | lstore_0 | 将栈顶 long 型数值存入第一个本地变量。 |
0x40 | lstore_1 | 将栈顶 long 型数值存入第二个本地变量。 |
0x41 | lstore_2 | 将栈顶 long 型数值存入第三个本地变量。 |
0x42 | lstore_3 | 将栈顶 long 型数值存入第四个本地变量。 |
0x43 | fstore_0 | 将栈顶 float 型数值存入第一个本地变量。 |
0x44 | fstore_1 | 将栈顶 float 型数值存入第二个本地变量。 |
0x45 | fstore_2 | 将栈顶 float 型数值存入第三个本地变量。 |
0x46 | fstore_3 | 将栈顶 float 型数值存入第四个本地变量。 |
0x47 | dstore_0 | 将栈顶 double 型数值存入第一个本地变量。 |
0x48 | dstore_1 | 将栈顶 double 型数值存入第二个本地变量。 |
0x49 | dstore_2 | 将栈顶 double 型数值存入第三个本地变量。 |
0x4a | dstore_3 | 将栈顶 double 型数值存入第四个本地变量。 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量。 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量。 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量。 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量。 |
push 系列指令
- 把一个整形数字(长度比较小)送到到栈顶。命令有一个参数,用于指定要送到栈顶的数字。
- 命令只能操作一定范围内的整形数值,超出该范围的使用将使用 ldc 命令。
指令码 | 助记符 | 说明 |
---|---|---|
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶。 |
0x11 | sipush | 将一个短整型常量值(-32768~32767)推送至栈顶。 |
dc 系列指令
- 把数值常量或 String 常量值从常量池中推送至栈顶。
- 该命令后面需要给一个表示常量在常量池中位置(编号)的参数。
- 对于 const 系列和 push 系列指令操作范围之外的数值类型常量,都放在常量池中。
- 所有不是通过 new 创建的 String 都是放在常量池中的。
指令码 | 助记符 | 说明 |
---|---|---|
0x12 | ldc | 将 int,float 或 String 型常量值从常量池中推送至栈顶。 |
0x13 | ldc_w | 将 int,float 或 String 型常量值从常量池中推送至栈顶(宽索引)。 |
0x14 | ldc2_w | 将 long 或 double 型常量值从常量池中推送至栈顶(宽索引)。 |
const 系列指令
- 把简单的数值类型送到栈顶。该系列命令不带参数。
- 只把简单的数值类型送到栈顶时,才使用该命令。
指令码 | 助记符 | 说明 |
---|---|---|
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)推送至栈顶。 |
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。 |
指令码 | 助记符 | 说明 |
---|---|---|
0x5f | swap | 将栈最顶端的两个数值互换(数值不能是 long 或 double 类型的)。 |
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 型数值作 " 按位异或 " 并将结果压入栈顶。 |
- 异常情况:仅规定了在处理整型数据时只有除法指令(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。
- 把数组的某项送到栈顶。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
指令码 | 助记符 | 说明 |
---|---|---|
0x2e | iaload | 将 int 型数组指定索引的值推送至栈顶。 |
0x2f | laload | 将 long 型数组指定索引的值推送至栈顶。 |
0x30 | faload | 将 float 型数组指定索引的值推送至栈顶。 |
0x31 | daload | 将 double 型数组指定索引的值推送至栈顶。 |
0x32 | aaload | 将引用型数组指定索引的值推送至栈顶。 |
0x33 | baload | 将 boolean 或 byte 型数组指定索引的值推送至栈顶。 |
0x34 | caload | 将 char 型数组指定索引的值推送至栈顶。 |
0x35 | saload | 将 short 型数组指定索引的值推送至栈顶。 |
- 把栈顶项的值存到数组里。该命令根据栈里内容来确定对哪个数组的哪项进行操作。
指令码 | 助记符 | 说明 |
---|---|---|
0x4f | iastore | 将栈顶 int 型数值存入指定数组的指定索引位置。 |
0x50 | lastore | 将栈顶 long 型数值存入指定数组的指定索引位置。 |
0x51 | fastore | 将栈顶 float 型数值存入指定数组的指定索引位置。 |
0x52 | dastore | 将栈顶 double 型数值存入指定数组的指定索引位置。 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置。 |
0x54 | bastore | 将栈顶 boolean 或 byte 型数值存入指定数组的指定索引位置。 |
0x55 | castore | 将栈顶 char 型数值存入指定数组的指定索引位置。 |
0x56 | sastore | 将栈顶 short 型数值存入指定数组的指定索引位置。 |
2.5 操作数栈管理指令
- 如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令。
指令码 | 助记符 | 说明 |
---|---|---|
0x57 | pop | 将栈顶数值弹出(数值不能是 long 或 double 类型的)。 |
0x58 | pop2 | 将栈顶的一个(long 或 double 类型的)或两个数值弹出(其它)。 |
0x59 | dup | 复制栈顶数值(数值不能是 long 或 double 类型的)并将复制值压入栈顶。 |
0x5a | dup_x1 | 复制栈顶数值(数值不能是 long 或 double 类型的)并将两个复制值压入栈顶。 |
0x5b | dup_x2 | 复制栈顶数值(数值不能是 long 或 double 类型的)并将三个(或两个)复制值压入栈顶。 |
0x5c | dup2 | 复制栈顶一个(long 或 double 类型的)或两个(其它)数值并将复制值压入栈顶。 |
0x5d | dup2_x1 | 复制栈顶数值(long 或 double 类型的)并将两个复制值压入栈顶。 |
0x5e | dup2_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。
- 从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。
- 在 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 human
hello 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();
}
}
/*print
hello man
hello 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.jianshu.com/p/d95cfde7fc49
https://blog.csdn.net/Alexwym/article/details/82152665