1.字节码指令
1.字节码总述
- 字节码格式
- 字节吗指令
- 类加载机制
- 字节码执行引擎
1.2 字节码指令简介
-
字节码中的方法表,方法表中存储的是方法的摘要信息,方法体存储在属性表(arrtibute_info)中的code中.
-
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(操作码 Opcode),以及跟随其后的零至多个代表此操作所需参数的参数(操作数 Operands)而构成。 (P196)
-
操作码的长度(占用class文件中大小)为1个字节(2^8),因此最大只有256条
-
JVM虚拟机采用面向操作数栈而不是棉线寄存器的架构.()
1.2 基于栈虚拟机和基于寄存器虚拟机区别
1.2.1 概述
任何一种虚拟机需要实现功能:
- 取指令,其中指令来源于内存
- 译码,决定指令类型(执行何种操作)。另外译码的过程要包括从内存中取操作数
- 执行。指令译码后,被虚拟机执行(其实最终都会借助于物理机资源)
- 存储计算结果
1.2.2 基于栈的虚拟机
- 概述
基于栈的虚拟机有一个操作数栈的概念,虚拟机在进行真正的运算时都是直接与操作数栈(operand stack)进行交互,不能直接操作内存中数据(其实这句话不严谨的,虚拟机的操作数栈也是布局在内存上的),也就是说不管进行何种操作都要通过操作数栈来进行,即使是数据传递这种简单的操作。 - 优点: 就是虚拟机可以无视具体的物理架构,特别是寄存器。
- 缺点: 就是速度慢,因为无论什么操作都要通过操作数栈这一结构。
由于执行时默认都是从操作数栈上取数据,那么就无需指定操作数。
例如,x86汇编”ADD EAX, EBX”,就需要指定这次运算需要从什么地方取操作数,执行完结果存放在何处。
但是基于栈的虚拟机的指令就无需指定,例如加法操作就一个简单的”Add”就可以了,因为默认操作数存放在操作数栈上,直接从操作数栈上pop出两条数据直接执行加法运算,运算后的结果默认存放在栈顶。
其中操作数栈(operand stack)的深度由编译器静态确定,方便给栈帧预分配空间。这个和不能再栈上定义变长数组相似(其实这句话不严谨的,栈上分配变长数组,需要编译器的支持,分配在栈顶),由于局部变量的地址只能在编译期(compile time)确定针对当前栈帧的offset,如果中间有一个变量是一个变长数组的话,那么后面变量的offset就无法确定了(vector的数据是分配在堆上的,自己控制)。
例如执行”a = b + c”
- 在基于栈的虚拟机上字节码指令如下所示:
由于操作数都是隐式地,所以指令可以做的很短,一般都是一个或者两个字节。但是显而易见就是指令条数会显著增加。
I1: LOAD C
I2: LOAD B
I3: ADD
I4: STORE A
- 基于寄存器虚拟机执行该操作只有一条指令:
其中a,b,c都是虚拟寄存器.
I1: add a, b, c
- 示例
操作数栈上的变化如下图所示:
首先从符号表(jvm局部变量表)上读取数据压入操作数栈:
然后从栈中弹出操作数执行加法运算,这步操作有物理机器执行,如下图所示:
从图示中可以看出,数据从局部变量表中还要经过一次操作数栈的操作,注意操作数栈和局部变量表都是存放在内存上,内存到内存的数据传输在x86的机器上都是要经过一次数据总线传输的。可以得出一次简单的加法基本上需要9次数据传输,想想都很慢。
但是基于栈的虚拟机优点就是可移植,寄存器由硬件直接提供。
使用栈架构的指令集,用户程序(编译后的字节码)不会直接使用硬件中的寄存器,同时为了提高运行时的速度,可以将一些访问比较频繁的数据存放到寄存器中以获取尽量好的性能。
另外,基于栈的虚拟机中指令更加紧凑,一个字节或者两个字节即可存储,同时编译器实现也比较简单,不用进行寄存器分配。寄存器分配是一门大学问。
1.2.3 基于寄存器的虚拟机
基于寄存器的虚拟机中没有操作数栈的概念,但是有很多虚拟寄存器,一般情况下这些寄存器(操作数)都是别名,需要执行引擎对这些寄存器(操作数)的解析,找出操作数的具体位置,然后取出操作数进行运算。
既然是虚拟寄存器,那么肯定不在CPU中(想想也不应该在CPU中,虚拟机的根本目的就是跨平台和兼容性),其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。
Lua虚拟机对寄存器的相关描述如下:
新的虚拟机也用栈分配活动记录,寄存器就在该活动记录中。当进入Lua程序的函数体时,函数从栈中分配一个足以容纳该函数所有寄存器的活动记录。函数的所有局部变量都各占据一个寄存器。因此,存取局部变量是相当高效的。
从上图中我们可以看到,其实“寄存器”的概念只是当前栈帧中一块连续的内存区域。这些数据在运算的时候,直接送入物理CPU进行计算,无需再传送到operand stack上然后再进行运算。
例如”ADD R3, R2, R1”的示意图就如下所示:
其实”ADD R3, R2, R1”还要经过译码的一个过程,当然当前这条指令的种类和操作数由虚拟机进行解释。后面我们会看到,在有些实现中,有一个很大的switch-case来进行指令的分派及真正的运算过程。
使用寄存器式虚拟机没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈(push/pop)指令。
同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的代码会比基于栈的代码要大,但是由于指令数量的减少,其实没有大多少。
1.2.4 总结
两种虚拟机实现的不同主要在于对操作数和结果的存储、检索机制不一样.
-
基于Stack的虚拟机会通过中断处理来获取操作数,其操作数保存在Stack数据结构中。从栈中取出数据、计算然后再将结果存入栈中(LIFO,Last in first out)。
-
- 1、代码必须使用这些指令来移动变量(即push和pop)
-
- 2、代码尺寸小和解码效率会更高些
-
- 3、堆栈虚拟机指令有隐含的操作数。
-
基于寄存器(Register)的虚拟机,其操作数存放在CPU寄存器中。没有入栈和出栈的操作,但是执行的指令因而需要包含操作数的地址,不可像栈那样用栈指针去操作。
-
- 1、使用堆栈来分配激活记录器
-
- 2、基于寄存器代码免去了使用push和pop命令的麻烦,减少了每个函数的指令总数。
-
- 3、代码尺寸和解码效率不如基于栈虚拟机,因为它包含操作数,所以指令大于基于堆栈的指令。但是基于寄存器产生更少的代码,所以总的代码数不会增加。
-
- 4、寄存器虚拟机必须从操作指令中解码操作数,需要额外的解码操作。
总结简述如下:
- 指令条数:栈式虚拟机多
- 代码尺寸:栈式虚拟机
- 移植性:栈式虚拟机移植性更好
- 指令优化:寄存器式虚拟机更能优化
栈式 VS 寄存器式 | 对比 |
---|---|
指令条数 | 栈式 > 寄存器式 |
代码尺寸 | 栈式 < 寄存器式 |
移植性 | 栈式优于寄存器式 |
指令优化 | 栈式更不易优化 |
解释器执行速度 | 栈式解释器速度稍慢 |
代码生成难度 | 栈式简单 |
简易实现中的数据移动次数 | 栈式移动次数多 |
2. 字节码与数据类型
- 在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。
iload:int型
fload:float型 - 大多数指令是包含类型信息的
- 也有不包含类型信息的
-
- Goto 与类型无关
-
- Arraylength 操作数组类型
- 类型多,指令少 8种数据类型+引用类型,如果每个每个指令包含类型信息,那256个指令就不够用了,一些指令将不支持的转换成支持的指令.
3 加载指令
3.1 概述
-
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
运算时会将局部变量表中的放入到操作数栈中,然后在操作数栈用运算结束后会放到局部变量表中.
-
将局部变量表加载到操作数栈:iload lload fload dload aload
-
将一个数值从操作数栈存储到局部变量表:istore lfda
-
将一个常量加载到操作数栈:bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst
-
扩充局部变量表的访问索引的指令:wide
-
将常量加载到操作数栈
– 当int取值-1~5采用iconst指令
– 取值-128~127采用bipush指令
– 取值-32768~32767采用sipush指令
– 取值-2147483648~2147483647采用 ldc 指
3.2 解读
// 源码
package classstruct;
public class HelloWorldMethod {
public int add(int a,int b){
return a+b;
}
private String conact(String str1,Object objStr){
return str1+objStr;
}
}
// 深入理解JVM2-class文件结构中的2.6.2 javap内容
#40 = Utf8 ()Ljava/lang/String;
{
public classstruct.HelloWorldMethod();
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 Lclassstruct/HelloWorldMethod;
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lclassstruct/HelloWorldMethod;
0 4 1 a I
0 4 2 b I
}
解读
lassstruct.HelloWorldMethod() 为构造方法.
args_size=1,是因为有默认有this一个参数.
stack=2, locals=3, args_size=3中
的 locals=3, 局部变量表大小
args_size=3 参数的个数,多的一个参数是默认的参数this所以为3个参数,所以是iload_1和iload_2(load_0是this)
public int add(int, int);为公用方法.
args_size=3,是因为除了参数a和b,还有this.
3.3 解读2
//源码
public class HelloWorldMethod2 {
public int add(int a,int b){
int c = a + b;
return 1+1;
}
}
//javap -verbose HelloWorldMethod2.class 的方法部分内容:
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iconst_2
5: ireturn
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcourse/jvmstu/opcode/HelloWorldMethod2;
0 6 1 a I
0 6 2 b I
4 2 3 c I
// 解读
istore_3 将操作数栈保存到局部变量表中
iconst_2 是常量值2,
ireturn 将常量返回.
和想象中不一样,没有将a+b的结果保存store,然后在将1+1的结果return,而是直接将常量1+1的结果返回,这是因为编译器优化了.
注意: main函数是静态类不会有默认this参数的.
编译器会将常量计算好到class中.
4 运算指令
4.1 概述
运算或算术指令用于对两个操作数栈上的值进行某种特定的运算,并把结果存储到操作数栈顶。
- 加法指令:add i l f d
- 减法指令:sub
- 乘法指令:mul
- 除法指令:div
- 取余指令:rem
- 取反指令:neg
操作栈顶的2个变量.
4.2 解读
package classstruct;
public class HelloOperation {
public int add(int a,int b){
int c = a + b;
int d = a - b;
int e = a * b;
int f = a / b;
int g = a % b;
int h = c + d + e + f + g;
return 1 + 1;
}
}
//截取的javap片段
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=9, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iload_1
5: iload_2
6: isub
7: istore 4
9: iload_1
10: iload_2
11: imul
12: istore 5
14: iload_1
15: iload_2
16: idiv
17: istore 6
19: iload_1
20: iload_2
21: irem
22: istore 7
24: iload_3
25: iload 4
27: iadd
28: iload 5
30: iadd
31: iload 6
33: iadd
34: iload 7
36: iadd
37: istore 8
39: iconst_2
40: ireturn
LineNumberTable:
line 5: 0
line 6: 4
line 7: 9
line 8: 14
line 9: 19
line 10: 24
line 11: 39
LocalVariableTable:
Start Length Slot Name Signature
0 41 0 this Lclassstruct/HelloOperation;
0 41 1 a I
0 41 2 b I
4 37 3 c I
9 32 4 d I
14 27 5 e I
19 22 6 f I
24 17 7 g I
39 2 8 h I
//会发现iload_1,iload_2,加载完之后然后操作,操做完之后然后就store了.然后再load1,laod2,然后操作再store
//istore 4 ,sotre到4,后面使用的时候再iload 4,
栈操作,栈的深度就为2(也就是a,和b的长度),计算完后的结果c,d…等在istore_3的时候就转存到了局部变量表中 .
5 类型转换指令
5.1 概述
- 类型转换指令可以将两种不同的数值类型进行相互转换,
-
- 这些转换操作一般用于实现用户代码中的显示类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题(栈中的字节码指令只有256个,如果每种类型对应一个字节码指令,那就不够用了.那就通过类型转换指令来操作)。
- 宽化类型处理(向上转型 int->long,UserVO->Object)和窄化类型处理(向下转型 long->int,Object->UserVO)
int i = 10; //4个字节
Long a = 10;//8个字节
int 的i会自动转成Long a,则为宽化类型处理(向上转型)
User user =getUser();
Ojbect obj = user;
这种也为宽化类型处理(向上转型)
Ojbect obj = getUser();
User user =(User)obj;
//这种情况为窄化类型处理(当然这种类型如果不正确会出错)
- i2b i2c i2s l2i …
-
- i2b int to byte
-
- i2c int to char
-
- i2s int to string
-
- l2i long to int
5.2 解读
//源码
package classstruct;
//指令转换
public class HelloInstruction {
public int add(int a,int b){
long c = a; //宽化类型处理
int d = (int)c;//窄化类型处
return 1 + 1;
}
}
//javap 结果片段
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=6, args_size=3
0: iload_1
1: i2l
2: lstore_3
3: lload_3
4: l2i
5: istore 5
7: iconst_2
8: ireturn
LineNumberTable:
line 5: 0
line 6: 3
line 7: 7
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lclassstruct/HelloInstruction;
0 9 1 a I
0 9 2 b I
3 6 3 c J
7 2 5 d I
}
SourceFile: "HelloInstruction.java"
package classstruct;
//指令转换
public class HelloInstruction2 {
public static int add(int a,int b){
long hour = 24;
long m = hour * 60 * 60 * 1000;
long mic = hour * 60 * 60 * 1000 * 1000;
System.out.println(mic/m);
return 1 + 1;
}
public static void main(String[] args) {
add(1,2 );
}
}
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=8, args_size=2
0: ldc2_w #2 // long 24l
3: lstore_2
4: lload_2
5: ldc2_w #4 // long 60l
8: lmul
9: ldc2_w #4 // long 60l
12: lmul
13: ldc2_w #6 // long 1000l
16: lmul
17: lstore 4
19: lload_2
20: ldc2_w #4 // long 60l
23: lmul
24: ldc2_w #4 // long 60l
27: lmul
28: ldc2_w #6 // long 1000l
31: lmul
32: ldc2_w #6 // long 1000l
35: lmul
36: lstore 6
38: getstatic #8 // Field java/lang/System.out:Ljav
a/io/PrintStream;
41: lload 6
43: lload 4
45: ldiv
46: invokevirtual #9 // Method java/io/PrintStream.prin
tln:(J)V
49: iconst_2
50: ireturn
LineNumberTable:
line 5: 0
line 6: 4
line 7: 19
line 8: 38
line 9: 49
LocalVariableTable:
Start Length Slot Name Signature
0 51 0 a I
0 51 1 b I
4 47 2 hour J
19 32 4 m J
38 13 6 mic J
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_1
1: iconst_2
2: invokestatic #10 // Method add:(II)I
5: pop
6: return
LineNumberTable:
line 13: 0
注意:HelloInstruction2为在JDK8上执行的结果,如果是在jdk中执行会造成结果等于,原因是先通过int计算(int 计算会造成丢失数据)然后转成long, 如下图:
6 对象创建与访问指令
6.1 概述
- 创建类实例的指令:new
- 创建数组的指令:newarray anewarray multianewarray
- 访问类字段 getfield putfield getstatic putstatic
- 把数组元素加载到操作数栈的指令:baload c s I l f d a
- 将操作数栈的值存储到数组元素:astore
- 取数组长度的指令:arraylength
- 检查实例类型的指令:instanceof checkcast
6.2 解读
//源码
package classstruct;
//指令转换
public class HelloUser {
public static void main(String[] args) {
User user = new User();
User[] us = new User[10];
int[] as = new int[10];
user.name = "username";
String name2 = user.name;
}
}
class User {
String name;
static int age;
}
//javap -verbose结果
{
public classstruct.HelloUser();
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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lclassstruct/HelloUser;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: new #2 // class classstruct/User
3: dup
4: invokespecial #3 // Method classstruct/User."<init>
":()V
7: astore_1
8: bipush 10
10: anewarray #2 // class classstruct/User
13: astore_2
14: bipush 10
16: newarray int
18: astore_3
19: aload_1
20: ldc #4 // String username
22: putfield #5 // Field classstruct/User.name:Lja
va/lang/String;
25: aload_1
26: getfield #5 // Field classstruct/User.name:Lja
va/lang/String;
29: astore 4
31: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 14
line 10: 19
line 11: 25
line 13: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 user Lclassstruct/User;
14 18 2 us [Lclassstruct/User;
19 13 3 as [I
31 1 4 name2 Ljava/lang/String;
}
SourceFile: "HelloUser.java"
//重要解读:
//以下静态类是没有默认this参数的. 所以args_size=1只有方法入参(这是和 ## 3.3 解读2 中有this默认入参的区别)
stack=2, locals=5, args_size=1
//以下为new 对象
0: new #2 // class classstruct/User
//以下为执行构造方法
4: invokespecial #3 // Method classstruct/User."<init>":()V
//以下为new 数组对象
10: anewarray #2 // class classstruct/User
//以下为对象赋值
22: putfield #5 // Field classstruct/User.name:Ljava/lang/String;
//以下为从对象取值
26: getfield #5 // Field classstruct/User.name:Ljava/lang/String;
操作数栈管理指令
- 操作数栈指令用于直接操作操作数栈。
- 将操作数栈的一个或两个元素出栈:pop pop2
- 复制栈顶一个或两个数值并将复制或双份复制值重新压入栈顶:dup dup2 dup_x1 dup_x2
- 将栈顶的两个数值替换:swap
8 控制转移指令
8.1 概述
- 控制转移指令可以让Java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在修改pc寄存器的值。
- 条件分支:ifeq iflt ifle ifne ifgt ifnull ifcmple…
- 复合条件分支:tableswitch lookupswitch
- 无条件分支:goto goto_w jsr jsr_w ret
8.2 解读
package classstruct;
public class HelloCondition {
public static void main(String[] args) {
int a = 10;
if(a>10){
System.out.println("a>10");
}else{
System.out.println("a<=10");
}
int b = 12;
if(b>10){
System.out.println("b>10");
}else{
System.out.println("b<=10");
}
}
}
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: bipush 10
2: istore_1
istore,a加载到操作数栈中
3: iload_1
10放入操作数栈
4: bipush 10
比较栈顶的2个数字,如果成功,继续往后,如果不成功跳转到20行
6: if_icmple 20
9: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
12: ldc #3 // String a>10
14: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
17: goto 28
20: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
23: ldc #5 // String a<=10
25: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
28: bipush 12
30: istore_2
31: iload_2
32: bipush 10
34: if_icmple 48
37: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
40: ldc #6 // String b>10
42: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
45: goto 56
48: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
51: ldc #7 // String b<=10
53: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
56: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 9
line 9: 20
line 12: 28
line 13: 31
line 14: 37
line 16: 48
line 18: 56
LocalVariableTable:
Start Length Slot Name Signature
0 57 0 args [Ljava/lang/String;
3 54 1 a I
31 26 2 b I
StackMapTable: number_of_entries = 4
frame_type = 252 /* append */
offset_delta = 20
locals = [ int ]
frame_type = 7 /* same */
frame_type = 252 /* append */
offset_delta = 19
locals = [ int ]
frame_type = 7 /* same */
}
SourceFile: "HelloCondition.java"
9 方法调用和返回指令
9.1 方法调用指令
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
- invokestatic指令用于调用类方法(static方法)。
9.2 解读调用指令
public interface Service {
int add(int a,int b);
}
public class ServiceImpl implements Service {
@Override
public int add(int a, int b) {
return a+b;
}
}
public class InvokerOpercode {
public static void main(String[] args) {
Service service = new ServiceImpl();
int retStr = service.add(1,2);
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class course/jvmstu/opcode/invo
ke/ServiceImpl
3: dup
4: invokespecial #3 // Method course/jvmstu/opcode/inv
oke/ServiceImpl."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: iconst_2
11: invokeinterface #4, 3 // InterfaceMethod course/jvmstu/o
pcode/invoke/Service.add:(II)I
16: istore_2
17: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
8 10 1 service Lcourse/jvmstu/opcode/invoke/Service;
17 1 2 retStr I
}
SourceFile: "InvokerOpercode.java"
//以下为调用接口
11: invokeinterface #4, 3
9.3 方法返回指令
方法的的调用指令与数据类型无关,而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
注意:即使void方法也会有一个return.
10 异常处理指令
10.1 概述
在程序中显式抛出异常的操作会由athrow指令实现,除了这种情况,还有别的异常会在其他Java虚拟机指令检测到异常状况时由虚拟机自动抛出。
10.2 解读
public class ExcepOpercode {
public static void main(String[] args) {
throw new RuntimeException("运行时异常了");
}
}
//javap -verbose main相关如下
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: new #2 // class java/lang/RuntimeExceptio
n
3: dup
4: ldc #3 // String 运行时异常了
6: invokespecial #4 // Method java/lang/RuntimeExcepti
on."<init>":(Ljava/lang/String;)V
9: athrow
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
}
SourceFile: "ExcepOpercode.java"
//以下为产生异常
// 9: athrow
10.3 try catch 使用解析
public class ExcepOpercode2 {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
int b = 100;
}
}
}
//javap -verbose main相关如下
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: iconst_1
1: iconst_0
2: idiv
3: istore_1
4: goto 11
7: astore_1
8: bipush 100
10: istore_2
11: return
Exception table:
from to target type
0 4 7 Class java/lang/Exception
LineNumberTable:
line 6: 0
line 9: 4
line 7: 7
line 8: 8
line 10: 11
LocalVariableTable:
Start Length Slot Name Signature
8 3 1 e Ljava/lang/Exception;
0 12 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
}
SourceFile: "ExcepOpercode2.java"
根据查看有try catch的语句,在不发生异常的情况下,try catch不影响任何性能.当有异常的时候通过Exception table来处理异常.(在旧的jdk下try catch 会有一定的性能影响)
How the Java virtual machine handles exceptions
11 同步指令
11.1 概述
- Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
- 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作(§2.11.8)之中。虚拟机可以从方法常量池中的方法表结构(method_info Structure,§4.6)中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
- 同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持(读者可参见§3.14中关于同步的描述)。
synchronized之后会有如下字节码:
monitorenter
monitoexit:
11.2 解读
public class SyncOpercode {
public static void main(String[] args) {
synchronized (SyncOpercode.class){
System.out.println("come to synchronized");
}
}
}
//javap -verbose main相关如下
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: ldc #2 // class course/jvmstu/opcode/sync
opercode/SyncOpercode
2: dup
3: astore_1
进入synchronized
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljav
a/io/PrintStream;
8: ldc #4 // String come to synchronized
10: invokevirtual #5 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
13: aload_1
退出synchronized
14: monitorexit
如果synhronized正常走完会走goto 23行,
15: goto 23
如果有异常则继续往后走
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
直接退出
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 5: 0
line 6: 5
line 7: 13
line 8: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "SyncOpercode.java"
进入synchronized
4: monitorenter
退出synchronized
14: monitorexit
如果synhronized正常走完会走goto 23行,
15: goto 23
如果有异常则继续往后走
18: astore_2
直接退出
23: return
11.3 管程
- 管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。
- 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计
- 系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对资源所执行的操作来表征该资源,而忽略了它们的内部结构和实现细节。
- 利用共享数据结构抽象地表示系统中的共享资源,而把对该共享数据结构实施的操作定义为一组过程。
12 参考文章
查寻常用的指令的含义
java 字节码 指令集 汇编