实战java虚拟机
深入理解java虚拟机
使用javap查看Class信息
字节码执行之于java虚拟机,如同汇编语言之于计算机一样重要。字节码是java虚拟机来说是执行的根本。当java源码被编译成class文件之后,虚拟机就会将class文件内的方法字节码载入系统并加以执行。
每一个java字节码指令是一个byte数字,并且有一个对应的住记符。目前所有的字节码指令大约有200个:
一个方法的java字节码指令,被编译到java方法的Code属性中,如果想要查看指令的具体内容,可用JDK自带javap工具。
-version 版本信息
-v -verbose 附加信息
-l Print line number and local variable tables
-public Show only public classes and members
-protected Show protected/public classes and members
-package Show package/protected/public classes
and members (default)
-p -private Show all classes and members
-c 对代码进行反汇编
-s Print internal type signatures
-sysinfo Show system info (path, size, date, MD5 hash)
of class being processed
-constants Show static final constants
-classpath <path> Specify where to find user class files
-bootclasspath <path> Override location of bootstrap class files
以下面这一段简单代码为例:
package cn.jhs.chap11;
public class Calc {
public int calc() {
int a = 500;
int b = 200;
int c = 50;
return (a+b)/c;
}
}
使用$ javap -v cn/jhs/chap11/Calc
查看:
##显示源文件名称#####
Classfile /E:/workspaces/idea/vm/vm_in_action/chap08/src/cn/jhs/chap11/Calc.class
Last modified 2018-7-14; size 276 bytes
MD5 checksum 349610279551c2967434c23c5ad574b5
Compiled from "Calc.java"
public class cn.jhs.chap11.Calc
SourceFile: "Calc.java"
##版本信息##
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
##常量池信息###
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // cn/jhs/chap11/Calc
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 calc
#9 = Utf8 ()I
#10 = Utf8 SourceFile
#11 = Utf8 Calc.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 cn/jhs/chap11/Calc
#14 = Utf8 java/lang/Object
{
###编译器自动插入,类的无参的构造函数
public cn.jhs.chap11.Calc();
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 8: 0
##calc方法##
public int calc();
flags: ACC_PUBLIC
Code:
##Stack表示操作数栈深度。操作数栈中最多有多少个参数。
##局部变量表大小:非static方法,第一个(index从0开始)局部变量都是this;
##入参个数(this)
stack=2, locals=4, args_size=1
0: sipush 500 ##自然偏移量0,将给定参数500压入操作数栈,sipush占1字节,int占2字节,故下一个字节码的偏移量为3.sipush处理范围 -32768~32767
3: istore_1 ##自然偏移量3,将操作数栈弹出一个元素,并存放在局部变量表第1个位置
4: sipush 200
7: istore_2
8: bipush 50 ##bipuch处理范围 -128~127
10: istore_3
11: iload_1 ##将局部变量表第一个变量 压入操作数栈
12: iload_2
13: iadd ##将操作数栈中两个数做加法运算
14: iload_3
15: idiv ##将操作栈中两个数做除法运算
16: ireturn ##将函数操作数栈顶层元素弹出,并压入调用者函数的操作数栈中。如果当前方法是synchronized,还隐含了一个monitorexit指令退出临界区。最终弹出当前函数的整个栈帧。
##行号信息##
LineNumberTable:
line 10: 0 ###代码第10行 与自然偏移量0有关。
line 11: 4
line 12: 8
line 13: 11
}
虚拟机规范中并没有明确的指定Slot应占用的内存大小,只是很有导向性的说到每个Slot都应该能存放一个
boolean,byte,char,short,int,float,reference或returnAddress类型数据。
java虚拟机常用指令
常量入栈指令
常量入栈指令是指将常熟压入操作数栈,根据数据类型和入栈内容的不同又可以分为const,push和ldc
系列。
- const:用于特定的常量入栈,入栈的常量隐含在命令中。
- 比如
aconst_null,将null压入操作数栈,iconst_m1将-1压入操作数栈,fconst_2将浮点数2压入栈。(iconst_x,x为0~5)
- i:表示整数,l表示长整型,f表示浮点数,d表示双精度浮点数,a表示对象引用。
- 比如
- push:主要包含bipush和sipush.它们的不同是bipush接收8位整数作为参数,sipush接收16位整数作为参数。
bipush 20 ; sipush 500;
- ldc:如果以上指令都无法满足需求,则使用万能指令ldc.它可以接收一个8位的参数,该参数指向常量池中int,float或者string的索引,并将指定内容压入操作数栈。
例如: ldc #27
- ldc 把常量池中的项压入栈
- ldc_w 把常量池中的项压入栈(使用宽索引)
- ldc2_w 把常量池中long类型或者double类型的项压入栈(使用宽索引)
局部变量表压栈
局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。以load
形式存在,这类指令包含:
- xload_n(x为x,l,f,d,a,n为0到3):将第n个局部变量压入操作数栈。例如:
iload_1,aload_2
- xload(x为i,l,f,d,a):使用此命令,此时局部变量超过4个时。比如:
aload 5;
- xaload(x为x,l,f,d,a,b,c,s):表示将数组的元素压栈。
xaload执行前,现将数组引用入栈(aload_1),再将数组索引(iconst_3)入栈
public void print3(short[] s){
System.out.println(s[3]);
}
编译后字节码指令为:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1 #####局部变量表1, 即数组 a 入栈
4: iconst_3 #####将数组索引 i 入栈
5: saload #####saload 将short数组元素压栈。
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
出栈装入局部变量表
出栈装入局部变量表表示指令将操作数栈中的栈顶元素弹出,装入局部变量的指定位置,用于给局部变量赋值。这类指令主要以store
形式存在。类似于load:
- xstore_n(x为x,l,f,d,a,n为0到3):istore_1 将栈顶元素,赋值给局部变量1
- xstore(x为i,l,f,d,a):由于没有隐含参数信息,故需要提供一个byte类型的参数来指定目标局部变量表的位置。
- xastore(x为x,l,f,d,a,b,c,s):如iastore它用于给一个int数组指定索引赋值。在执行iastore执行前,操作数栈顶需要准备3个元素:值,索引,数组引用
java代码:
public void print4(int[] s){
int i,j,k;
i = 21;
s[3]=77;
}
编译后字节码指令为:
0: bipush 21 ###常量21 入栈
2: istore_2 ###21 赋值给局部变量2
3: aload_1 ###加载局部变量1,即数组 a
4: iconst_3 ###常量3入栈,索引 i,
5: bipush 77 ###常量77入栈, 值 value
7: iastore ###数组赋值
8: return
通用型操作
从上述的几类指令中可以看到,大部分的数据操作指令时和数据类型相关的。java虚拟机为不同的数据类型都量身定做了一些列的指令。但是无类型的指令还是必要的,通用型操作就提供了这种无需指明数据类型的操作。
- NOP:是一个非常特殊的指令,它的字节码是0x00,和汇编的nop一样,
它表示什么也不做。这条指令一般可用于调试、占位等
- dup:duplicate复制,将栈顶元素复制一份并在此压入栈顶,这样栈顶就有2个一模一样的元素。
dup2
- pop:把一个元素从栈顶弹出,并且直接废弃。
pop一个字长(32位),要丢弃64位数据,可使用pop2
java源码示例:
public void print4(int i){
Object obj = new Object();
obj.toString();
}
编译后字节码指令为:
###使用创建对象指令new,创建文成后,将对象引用放置在栈顶
0: new #4 // class java/lang/Object
3: dup ###dup复制一份对象的引用
##执行调用对象的构造函数,此时需要将对象引用,故会将栈顶元素弹出一次
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_2 ###将栈顶元素 复制给局部变量表2
8: aload_2 ###局部变量表2 ,入栈
9: invokevirtual #5 // Method java/lang/Object.toString:()Ljava/lang/String;
12: pop
13: return
类型转换指令
在软件开发的过程中,数据类型转换是很常见的功能。为了很好的支持类型转换,虚拟机提供了一整套专门用于类型转换的指令。这类命令基本上使用x2y的形式给出。i2l,将i转换为long数据。指令i2l执行时,首先将int数据入栈,然后进行转换,然后进行赋值(出栈)。
public void print5(int i) {
long l = i;
int j = (int) l;
}
}
编译后字节码指令为:
0: iload_1
1: i2l
2: lstore_2
3: lload_2
4: l2i
####这里需要注意的地方,为什么不是istore_3,而是istore_4。是因为long是64位数据类型,会在局部变量表中占用两个位置。
5: istore 4
7: return
实际上指令中有i2b,i2c,i2s,但是并没有b2i,c2i,s2i.
- 对于byte转换为int,虚拟机并没有做实质化的处理。
- 对于byte转long,使用的指令时i2l
,在虚拟机内部,此时已经将byte等同于int处理。
为什么会这样处理呢?
一方面可以减少指令数量。 因为目前虚拟机只有用一个字节来表示指令,最多只能支持256个指令,为了节省指令资源。
另一方面,无论是byte,short在局部变量表中都会占用32位空间,没有必要特意区分集中数据类型。
运算指令
- 加法:xadd(i,l,f,d,代表int,long,float,double);
- 减法:xsub
- 乘法:xmul
- 除法:xdiv
- 取余:xrem
- 取相反数:xneg
- 自增指令:iinc。
iinc index,value ; index表示在局部变量表位置,value表示自增的值。
- 位运算
- 位移指令:ishl,ishr;lshl,lshr;
iushr,lushr 将int,long右移动(无符号)
- 或:ior,lor
- 与:iand,land
- 异或:ixor
- 位移指令:ishl,ishr;lshl,lshr;
对象/数组操作指令
(1).创建对象
- new :创建普通对象。
- newarray:创建基本类型数组。newarray int ,newarray long..
- anewarray:创建对象数组。anewarray #3
- multianewarray :创建多维数组。
public void print8() {
long[] a1 = new long[10];
String[] a2 = new String[3];
int[][] a3 = new int[5][6];
}
编译后字节码指令为:
0: bipush 10
2: newarray long
4: astore_1
5: iconst_3
6: anewarray #10 // class java/lang/String
9: astore_2
10: iconst_5
11: bipush 6
13: multianewarray #11, 2 // class "[[I" ####2表示2纬数组。
17: astore_3
18: return
(2).字段访问指令
- getfield:实例对象字段。
实例对象字段访问,会比类静态字段访问多一个值操作数。即实例变量自身。
- putfield
- getstatic: 类静态字段。
- putstatic
实例代码:
class User{
public String name = "aa";
public static int type = 3;
}
public void print9(User user){
String tempName = user.name;
user.name = "bb";
int tempType = User.type;
User.type = 33;
}
编译后字节码指令为:
0: aload_1
1: getfield #13 // Field cn/jhs/chap11/User.name:Ljava/lang/String;
4: astore_2 ####将局部变量1的,#13字段值,保存至局部变量2.
5: aload_1
6: ldc #14 // String bb
8: putfield #13 // Field cn/jhs/chap11/User.name:Ljava/lang/String;
11: getstatic #15 // Field cn/jhs/chap11/User.type:I
14: istore_3
15: bipush 33
17: putstatic #15 // Field cn/jhs/chap11/User.type:I
20: return
(3).类型检查指令
- checkcast:强制转换是否可以进行,如果可以指令不会改变操作数栈,如果不可行抛出ClassCastException.
- instanceof:判断给定对象是否是某一个类的示例,它会将判断结果压入操作数栈。
代码示例:
public String checkcast(Object obj) {
if(obj instanceof String){
return (String) obj;
}
return null;
}
编译后字节码指令为:
0: aload_1
1: instanceof #10 // class java/lang/String
4: ifeq 12
7: aload_1
8: checkcast #10 // class java/lang/String
11: areturn
12: aconst_null
13: areturn
(4).数组操作指令
- xastore
- xaload
- arraylenth: 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
比较控制指令
(1).比较指令
比较指令的作用是比较栈顶两个元素的大小,并将结果入栈。 dcmpg,dcmpl ; fcmpg,fcmpl ; lcmp
首字符d表示double,f表示float,l表示long.对于数字double,float,由于有NaN的存在,故各有两个版本。
栈顶为v1,栈顶顺位第2元素是v2, 若v1=v2,则压入0;若v1>v2 ,则压入1
;若v1<v2则压入-1;若遇到NaN值,fcmpg会压入1,fcmpl则压入-1;
(2).条件跳转指令
条件跳转指令通常和比较指令结合使用。条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull
这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset).它们的统一含义是:弹出栈顶元素,测试它是否满足某一条件,如果满足则跳转到指定位置。在条件跳转执行前,一般可以先用比较指令进行栈顶元素的准备,然后再进行条件跳转。
代码示例:
public void cmp1() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 > f2);
}
编译后字节码指令为:
0: ldc #16 // float 9.0f
2: fstore_1
3: ldc #17 // float 10.0f
5: fstore_2
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
9: fload_1
10: fload_2
11: fcmpl #### 比较 fload_1 < fload_2,并将结果压入栈顶
12: ifle 19 ####获取栈顶比较结果,如果 <= 则跳转到19行,否则书序执行
15: iconst_1
16: goto 20
19: iconst_0
20: invokevirtual #18 // Method java/io/PrintStream.println:(Z)V
23: return
(3).比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体。类似指令有:if_icmpeq,if_icmpne,if_icmpgt,if_icmplt,if_icmple,if_icmpge和if_acmpeq,if_acmpne
,其中助记符加上“if_”后,以字符“i”开头的指令指针对int整数操作(包含short和byte类型),以字符“a”开头的指令表示对象引用比较。运行此指令时,栈顶需要准备两个元素进行比较。
9: iload_1
10: iload_2
11: if_icmple 18
(4).多条件分支跳转
多条件分支跳转指令时转为switch-case语句设计的,主要有tableswitch和lookupswitch。
- tableswitch:条件分支值是连续的,它内部只存放起始值核终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量上,因此效率较高。
- l**ookupswitch**:内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。
(5).无条件跳转
目前主要的无条件跳转指令为goto
.指令jsr,ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃。如果指令偏移量太大,则它可以使用goto_w
,它接收4个字节作为操作数。
函数调用与返回指令
- invokevirtual:虚函数调用,调用对象的实例方法,根据对象的实际类型进行转发,支持多态,是目前最常用的java函数调用方式。
- invokeinterface:指接口方法的调用,当被调用对象申明为接口时,使用该指令。
- invokespecial:调用特殊方法,比如构造方法,类的私有方法,父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态绑定。
- invokestatic:调用类静态方法。
- invokedynamic:调用动态绑定的方法,JDK1.7后新加入的指令。
函数调用结束前,需要进行返回。返回时,需要使用xreturn指令将返回值存入调用者的操作数栈中。当返回int时,ireturn;当返回值是void时,使用指令return,返回值为引用类型时,使用areturn.
示例代码:
public String rtn(){
String msg = "hello world";
System.out.println(msg);
return msg;
}
编译后字节码指令为:
0: ldc #19 // String hello world
2: astore_1
3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
###调用PrintStream.void()方法
7: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: aload_1
11: areturn ##### areturn
同步控制
为了实现多线程同步,java虚拟机提供了monitorenter和monitorexit两条指令来完成临界区的进入和离开操作。
> 当一个线程进入同步块时,它使用monitorenter指令请求进入,如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断当前监视器的线程是否为自己,如果是,则进入,否则进行等待,知道对象的监视器计数为0,它才会被允许进入同步块。当线程退出同步块时,需要使用monitorexit申明退出。
实例代码:
private int i = 0;
public synchronized void add1(){
i++;
}
public void add2(){
synchronized (this) {
i++;
}
}
编译后字节码指令为:
//add1():没有使用monitorenter和monitorexit进行同步区控制,但是当虚拟机通过方法标识符判断是一个同步方法时,会自动在方法调用前进行加锁。
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
//add2():有两个monitorexit,第二个monitorexit结合着athrow指令使用。虽然在源码中没有进行异常处理,但是在字节码中会自动插入
0: aload_0
1: dup
2: astore_1
3: monitorenter ###monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit ###第一个monitorexit
16: goto 24 ###正常,则跳转24,直接结束方法。
19: astore_2 ####异常处理节点....
20: aload_1
21: monitorexit ####第二个monitorexit
22: aload_2 ####异常信息,压入栈顶
23: athrow
24: return