您的位置:首页 > 编程语言 > Java开发

Java虚拟机常用指令(二十二)

2017-06-25 15:26 253 查看
常量入栈指令

该指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列,push系列和ldc指令;

const:用于特定的常量入栈,入栈的常量隐含在指令本身里。
比如:aconst_null将null压入操作数栈;iconst_m1将-1压入操作数栈;
指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出;

push:主要包括bipush和sipush,它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈;

ldc:可接收一个8位的参数,该参数指向常量池中的int,float或者String的索引,将制定的内容压入堆栈;
类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc;
如果要压入的元素是long或者double,则使用ldw2_w指令;

局部变量压栈指令

该指令将给定的局部变量表中的数据压入操作数栈。
这类指令大体可以分为:xload(x为i,l,f,d,a),xload_n(x为i,l,f,d,a,n为0到3),xaload(x为i,l,f,d,a,b,c,s);

x的取值表示数据类型:



指令xload_n
表示将第n个局部变量压入操作数栈,比如iload_1,fload_0,aload_0等指令。其中aload_n表示将一个对象引用压栈;

指令xload
通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload,fload等;

指令xaload
表示将数组的元素压栈,比如saload,caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈;

出栈装入局部变量表指令
该指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。这类指令主要以store的形式存在,比如xstore(x为i,l,f,d,a),xstore_n(x为i,l,f,d,a,n为0到3)和xastore(x为i,l,f,d,a,b,c,s)。x的取值含义和load类命令是一样的。

指令istore_1
从操作数栈中弹出一个整数,并把它赋值给局部变量1。

指令xstore
没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置;

指令xastore
专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值;在iastore执行前,操作数栈订需要以此准备3个元素:值,索引,数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置;

示例:

 



它生成的字节码中包含istore和iastore指令





其中,第0行字节码,压入常量99,第2行istore将99弹出,并赋给局部变量表6的变量,而该变量正好是X。接着在第4,5,6行分别压入iastore所需的3个参数,

最后调用iastore将77赋值给局部变量表第2位(int[] s)数组的第0个索引位置(s[0])

通用型操作
该操作提供了无需指明数据类型的操作。比如栈操作,不是在所有时刻对栈的压入或者弹出都必须明确数据类型的。

指令NOP
字节码为0x00,表示什么也不做。这条指令一般用于调式,占位等;

指令dup
意为duplicate复制,会将栈顶元素复制一份并再次压入栈顶,这样栈顶就有两份一摸一样的元素了;

指令pop
把一个元素从栈顶弹出,并且直接废弃

示例:指令dup与pop的说明

 



  

编译成字节码后,内容如下:





为了生成Object对象,使用了对象创建指令new。创建完成后,new指令会把对象引用放置在栈顶,此时,栈顶只有一份对象引用。但是,在new指令之后,对该obj对象连续进行两次操作:一次是通过invokespecial指令调用对象的构造函数,另一次是通过astore_2将对象赋值给obj。这两个操作都会将栈顶元素弹出,故为了连续两次使用同样的栈顶元素,这里使用指令dup赋值了一份对象引用,供后面连续两次指令使用。在obj.toString()方法执行完毕后,函数的范围值会出现在栈顶,但是由于没有人使用,故简单地使用pop操作将无人问津的返回值直接丢弃;

注意:pop指令只能丢弃一个字长(32位),如果要丢弃栈顶64位数据(long或者double),则需要使用pop2命令,类似地,如果要连续复制栈顶2个字长,则可以使用dup2指令

类型转换指令

该指令专门用于类型转换;这类指令的助记符使用x2y的形式给出。其中x可能是i,f,l,d,y可能是i,f,l,d,c,s,b。它们的含义见下表:



比如:i2l表示将int数据转为long数据。指令i2l在执行时,先将栈顶的int数据弹出,然后进行转换。最后,将转化后的long型数据压入,如下图,转换后的Long型数字占用两个字空间;



示例:



其字节码指令如下:



查看加粗的字节码,int转换为long使用了i2l,long转换为float使用了l2f,
long转换为int使用了l2i;

示例:byte转换为int和long

 







对于byte类型转为int,虚拟机并没有做实质性的转化处理,只是简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是i2l,可以看到在内部byte在这里等同于int处理,类似的还有short。这种处理方式有两个特点:
1.可以减少实际的数据类型,如果为short和byte都准备一套指令,那么指令的数量会大增,而虚拟机目前的设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将short和byte当作int处理;
2.由于局部变量表中的槽位固定为32位,无论是byte或者short存入局部变量表,都会占用32位空间,从这个角度说,也没有必要特意区分这几种数据类型;

运算指令
该指令为虚拟机提供基本的加减乘除运算功能;每种指令也有自己支持的数据类型,使用一个字符表示:





以乘法指令为例,imul表示从操作数栈中弹出两个整数,将它们相乘,结果再压入栈。指令lmul表示long型,fmul表示对float的操作,dmul表示对double的乘法;

取余指令用于计算两个数相除后的余数,比如,i%j就会产生irem指令;

示例:数值取反指令改变数字的符号位



生成的字节码如下:



指令fneg操作前后,操作数栈没有变化,只是栈顶的元素符号位被取反;
 

示例:指令iinc对给定的局部变量做自增操作,这条指令是少数几个执行过程中完全不修改操作数栈的指令。它接收两个操作数:

第1个局部变量表的位置,第2个位累加数。比如常见的i++,就会产生这条指令



生成的字节码如下:





在参数中,this,j占据了局部变量表的第0和第1个位置,故i处于第2个位置

示例:位运算指令由位运算符产生,除了按位取反,其它的位运算符都有对应的指令,现在演示按位取反:



生成的字节码:



ixor为整数的按位异或操作,它从栈中弹出两个整数,并将它们按位异或,将结果再压入栈中。

在ixor执行前,压入栈中的数字为i=123以及-1(iconst_ml),因此,在虚拟机中,按位取反是通过与-1异或计算得来的。

注意:-1的2进制表示为一个全1的数字0xFF,任何数字与0xFF异或后,自然取反;

对象/数组操作指令
对于对象的操作指令,可进一步细分为创建指令,字段访问指令,类型检查指令,数组操作指令

创建指令
用于创建对象或数组,主要有:new,newarray,anewarray和multianewarray;
该接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈;
示例:指令newarray和anewarray用来创建数组。前者用于创建基本类型的数组,后者用于创建对象数组。指令multianewarray用于创建多维数组。



这段代码创建了一个int数组,一个Object数组和一个int二维数组,因此它依次使用了newarray,anewarray和multianewarray;



字段访问指令
该指令专门用于访问类或者对象的字段;主要有:getfield,putfield,getstatic,pustatic4个;
getfield,putfield用于操作实例对象的字段,getstatic,pustatic用于操作类的静态字段;
示例:

 



java编译器会为这条语句产生如下getstatic指令,用于将system.out这个静态字段压入操作数栈。这段指令显示,常量池第21号为Fieldref,它指向System.out静态字段,字段类型为java/io/PrintStream



类型检查指令
该指令有两个:checkcast,instanceof
checkcast:用于检查类型强制转换是否可以进行。如果可以进行,checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常;
instanceof:用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈;
示例:



该代码使用了instanceof关键字,并使用了强制转换,它们分别会产生instanceof和checkcast两个字节码



它们都接收一个操作数,并判断栈顶层元素是否可以转为该操作数给定的类型

比较控制指令
该指令代表条件控制。大体上分为比较指令,条件跳转指令,比较条件跳转指令,多条件分支跳转,无条件跳转指令等;

比较指令
作用:比较栈顶两个元素的大小,并将比较结果入栈。

指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp;首字符d表示double类型,f表示float,l表示long;对于double和float的数字,由于NaN的存在,所有有两个版本,以float为例,有fcmpg和fcmpl两个指令,它们的区别在于数字比较时,若遇到NaN值,处理结果不同;
指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0,若v1>v2则压入1,若v1<v2则压入-1。两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl压入-1
指令dcmpg,dcmpl类似;

跳转指令
作用:该指令一般与比较指令结合使用。
指令:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull;这些指令都接收两个字节的操作数,用于计算跳转的位置。
统一含义:弹出栈顶元素,测试它是否满足某一个条件,如果满足条件,则跳转到给定位置。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后再进行条件跳转;
示例:





第9行和第10行在栈顶准备了两个比较元素。第11行指令对栈顶两个元素进行比较,第12行ifle获取栈顶的结果,并确认是否需要跳转

比较条件跳转指令
该指令类似于比较指令和条件跳转指令的结合体;

指令:if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne;
为助记符加上"if_"后,以字符"i"开头的指令针对int整数操作(包括short和byte),以字符"a"开头的指令表示对象引用的比较;
这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句;

示例:

 





第9和第10行将需要比较的数字压入栈,第11行执行比较,如果条件成立则跳转18行输出0,否则继续执行后一条指令输入1

示例:如果比较的元素是对象,那么就会使用if_acmpeq和if_acmpne指令

 





第9,10行和第35,36行,分别压入栈顶元素比较,第21和第37行执行对象引用的比较并确认是否需要跳转

多条件分支跳转

专为switch-case语句设计。主要有tableswitch和lookupswitch;

区别:
tableswitch:要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量,因此效率比较高;
lookupswitch:内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低;



由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,给定的index值,计算出对应的offset。



lookupswitch处理的是离散的case值,但是处于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default

示例:tableswitch指令





由于case值是连续的,编译器生成了tableswitch指令来处理switch

示例:lookupswitch指令



  



case值不连续,使用lookupswitch指令。从字节码体积上看,lookupswitch占用的空间更多。

示例:JDK1.7中,switch对字符串的处理,使用的是lookupswitch指令







为了支持String的switch操作,在字节码第三行调用了字符串的hashCode()方法,得到int整数。在lookupswitch指令中,实际使用该hash值作为分支的case.
如果hash值没有匹配的,则必然字符串也没有匹配的,因此可以直接执行default出的指令,但如果hash值匹配,考虑到hash冲突的存在,这里并没有进行匹配后的指令,还是对匹配进行二次确认。

在第43行使用String.equals()函数判断字符串是否真的相等。如果确实相等,则执行对应的语句,否则跳转退出。

综上,当使用String作为case类型时,虚拟机要多执行hash计算以及字符串相等等操作,性能也会低于直接对int的处理

无条件跳转
该跳转指令为goto。指令jsr,ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机废弃;
goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏离量给定的位置处;
如果偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但它接收4个字节作为操作数,可以达到更宽的地址范围;

函数调用与返回指令
使虚拟机支持函数调用
函数调用指令:invokevirtual,invokeinterface,invokespecial,invokestatic,invokedynamic;
函数返回:需要将返回值压入调用者操作数栈,需要使用xreturn指令(x可以是i,l,f,d,a或空)

函数指令的作用范围:
invokevirtual:虚函数调用,调用对象的实例方法,根据对象的实际类型进行派发,支持多态;
invokeinterface:指接口方法的调用,当被调用对象申明为借口时,使用该指令调用接口的方法;
invokespecial:调用特殊的一些方法,比如构造函数,类的私有方法,父类的方法。这些方法是静态类型绑定的,不会在调用时进行动态派发;
invokestatic:调用类的静态方法,这个也是静态绑定的;
invokedynamic:调用动态绑定的方法,JDK1.7新加入的指令;

函数调用结束前,需要进行返回。返回时,使用xreturn指令将返回值存入调用者的操作数栈中。根据返回值,该指令的前缀会不同。
返回int时,指令为ireturn,返回为void时,使用return;
该指令被调用时,如果方法是同步的,那么调用后,监视器锁将被释放;

示例:指令invokevirtual的使用



以上代码产生如下字节码:



invokevirtual指令在这里调用了PrintStream实例的println()方法。调用前,操作数栈中将压入调用对象的实例,以及该函数的所有参数。

在本例中为System.out实例和字符"aa"。

指令invokevirtual需要两个字节作为操作数,用于计算指向常量池的索引,这里索引必须指向CONSTANT_Methodref入口,表示需要调用的方法;

示例:invokeinterface指调用接口的函数:



该代码生成了Thread对象,并调用了它的run()方法。Thread类实现了Runnable接口。

这里使用两种方式调用run():

第一种直接在Thread申明的实例上调用;

第二种将其转为接口类型Runnable,再进行调用;

这两种调用方式使用的invoke指令是不同的,如左下的图;

  



字节码指令中第9行,使用invokevirtual指令,是直接针对Thread对象的调用,第13行,则是针对Runnable接口的调用。和invokervirtual不同,invokeinterface在调用时,需要额外传入1个字节,作为无符号整数,表示这次函数调用所需参数的字数(1字为32位),包含隐含的this。

本例,函数没有参数,只需要当前引用this,故数字为1

  

示例:invokespecial用于调用特殊的函数,该指令调用时,接收两个字节作为其操作数,用于计算常量池索引入口,且该入口必须为CONSTANT_Methodref。

 





指令invokespecial调用了Date类的构造函数

示例:调用类的私有方法;由于类的私有方法不具有多态性,即使在子类中有相同签名的私有方法,也不能覆盖父类中对应的私有方法的行为,因此对于私有方法调用可以使用静态绑定









invokespecial在调用父类方式,通过操作数直接指向父类的toString()方法,从而避免了子类toString()方法的使用

同步控制

Java虚拟机提供了monitorenter,monitorexit来完成临界区的进入和离开。达到多线程的同步;
当一个线程进入同步快时,它使用monitroenter指令请求进入,如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块;
当线程退出同步块时,需要使用monitorexit申明退出。
在java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态;

monitorenter,monitorexit在执行时,都需要在操作数栈顶压入对象,之后,monitorenter,monitorexit的锁定和释放都是针对这个对象的监视器

图示:当线程4离开临界区后,线程1,2,3才有可能进入



示例:monitorenter,monitorexit使用示例





在类SyncAdd方法中,有方法add1()和add2(),它们都对当前this对象进行加锁,并对实例字段i进行更新。对于add1(),字节码如下:



此段代码和无同步的代码没有什么区别,没有monitroenter和monitorexit进行同步区控制。

因为对于同步方法而言,当虚拟机通过方法的访问标识符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方式而言,monitroenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。

add2()的字节码如下:





该段字节码的解析如下:

第0行将this引用入栈;

第1行复制this引用,并入栈;

第2行将this引用弹出,存入第1个局部变量:

第3行根据栈顶的this引用进行加锁;

第4~14行执行了i++操作;

第15行表示释放锁,此时,i++已经完成;

第16行跳转到第22行,并退出;

如果在第4~16行执行期间,遇到任何异常,则进入第19行处理;

第19行将第1个局部变量入栈,该变量就是this,由第2行存入。

第20行根据栈顶的this,退出临界区,释放锁;

第21行抛出当前发生的异常,异常对象位于栈顶;

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 虚拟机