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

  • 常量入栈指令
    • 该指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列,push系列和ldc指令;
      • const:用于特定的常量入栈,入栈的常量隐含在指令本身里。
        • 比如:aconst_nullnull压入操作数栈;iconst_m1-1压入操作数栈;
        • 指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出;
      • push:主要包括bipushsipush,它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈;
      • ldc:可接收一个8位的参数,该参数指向常量池中的intfloat或者String的索引,将制定的内容压入堆栈;
        • 类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc
        • 如果要压入的元素是long或者double,则使用ldw2_w指令;
  • 局部变量压栈指令
    • 该指令将给定的局部变量表中的数据压入操作数栈。
      • 这类指令大体可以分为:xload(xi,l,f,d,a)xload_n(xi,l,f,d,a,n03),xaload(x为i,l,f,d,a,b,c,s)
      • x的取值表示数据类型:
        • Java虚拟机常用指令(二十二)_第1张图片
    • 指令xload_n
      • 表示将第n个局部变量压入操作数栈,比如iload_1,fload_0,aload_0等指令。其中aload_n表示将一个对象引用压栈;
    • 指令xload
      • 通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload,fload等;
    • 指令xaload
      • 表示将数组的元素压栈,比如saloadcaload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈;
  • 出栈装入局部变量表指令
    • 该指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。这类指令主要以store的形式存在,比如xstore(xi,l,f,d,a)xstore_n(x为i,l,f,d,a,n03)xastore(xi,l,f,d,a,b,c,s)x的取值含义和load类命令是一样的。
    • 指令istore_1
      • 从操作数栈中弹出一个整数,并把它赋值给局部变量1
    • 指令xstore
      • 没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置;
    • 指令xastore
      • 专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值;在iastore执行前,操作数栈订需要以此准备3个元素:值,索引,数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置;
    • 示例:

 

它生成的字节码中包含istoreiastore指令

Java虚拟机常用指令(二十二)_第2张图片

其中,第0行字节码,压入常量99,第2istore99弹出,并赋给局部变量表6的变量,而该变量正好是X。接着在第4,5,6行分别压入iastore所需的3个参数,
最后调用
iastore77赋值给局部变量表第2位(int[] s)数组的第0个索引位置(s[0]

  • 通用型操作
    • 该操作提供了无需指明数据类型的操作。比如栈操作,不是在所有时刻对栈的压入或者弹出都必须明确数据类型的。
    • 指令NOP
      • 字节码为0x00,表示什么也不做。这条指令一般用于调式,占位等;
    • 指令dup
      • 意为duplicate复制,会将栈顶元素复制一份并再次压入栈顶,这样栈顶就有两份一摸一样的元素了;
    • 指令pop
      • 把一个元素从栈顶弹出,并且直接废弃
    • 示例:指令duppop的说明

 

  

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

Java虚拟机常用指令(二十二)_第3张图片

为了生成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型数字占用两个字空间;
        • Java虚拟机常用指令(二十二)_第4张图片
    • 示例:

其字节码指令如下:

Java虚拟机常用指令(二十二)_第5张图片

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

  • 示例:byte转换为intlong

 

Java虚拟机常用指令(二十二)_第6张图片

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

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

Java虚拟机常用指令(二十二)_第7张图片

以乘法指令为例,imul表示从操作数栈中弹出两个整数,将它们相乘,结果再压入栈。指令lmul表示long型,fmul表示对float的操作,dmul表示对double的乘法;
取余指令用于计算两个数相除后的余数,比如
,i%j就会产生irem指令;

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

生成的字节码如下:

Java虚拟机常用指令(二十二)_第8张图片

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

  • 示例:指令iinc对给定的局部变量做自增操作,这条指令是少数几个执行过程中完全不修改操作数栈的指令。它接收两个操作数:
    1个局部变量表的位置,第2个位累加数。比如常见的i++,就会产生这条指令

生成的字节码如下:

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

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

生成的字节码:

Java虚拟机常用指令(二十二)_第9张图片

ixor为整数的按位异或操作,它从栈中弹出两个整数,并将它们按位异或,将结果再压入栈中。
ixor执行前,压入栈中的数字为i=123以及-1(iconst_ml),因此,在虚拟机中,按位取反是通过与-1异或计算得来的。

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

  • 对象/数组操作指令
    • 对于对象的操作指令,可进一步细分为创建指令,字段访问指令,类型检查指令,数组操作指令
    • 创建指令
      • 用于创建对象或数组,主要有:new,newarray,anewarraymultianewarray
      • 该接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈;
      • 示例:指令newarrayanewarray用来创建数组。前者用于创建基本类型的数组,后者用于创建对象数组。指令multianewarray用于创建多维数组。

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

Java虚拟机常用指令(二十二)_第10张图片

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

 

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

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

Java虚拟机常用指令(二十二)_第11张图片

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

Java虚拟机常用指令(二十二)_第12张图片

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

  • 比较控制指令
    • 该指令代表条件控制。大体上分为比较指令,条件跳转指令,比较条件跳转指令,多条件分支跳转,无条件跳转指令等;
    • 比较指令
      • 作用:比较栈顶两个元素的大小,并将比较结果入栈。
      • 指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp;首字符d表示double类型,f表示floatl表示long;对于doublefloat的数字,由于NaN的存在,所有有两个版本,以float为例,有fcmpgfcmpl两个指令,它们的区别在于数字比较时,若遇到NaN值,处理结果不同;
        • 指令fcmpgfcmpl都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0,若v1>v2则压入1,若v1则压入-1。两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl压入-1
        • 指令dcmpg,dcmpl类似;
    • 跳转指令
      • 作用:该指令一般与比较指令结合使用。
      • 指令:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull;这些指令都接收两个字节的操作数,用于计算跳转的位置。
      • 统一含义:弹出栈顶元素,测试它是否满足某一个条件,如果满足条件,则跳转到给定位置。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后再进行条件跳转;
      • 示例:

Java虚拟机常用指令(二十二)_第13张图片

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

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

 

Java虚拟机常用指令(二十二)_第14张图片

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

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

 

Java虚拟机常用指令(二十二)_第15张图片

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

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

Java虚拟机常用指令(二十二)_第16张图片

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

Java虚拟机常用指令(二十二)_第17张图片

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

  • 示例:tableswitch指令

Java虚拟机常用指令(二十二)_第18张图片

Java虚拟机常用指令(二十二)_第19张图片

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

  • 示例:lookupswitch指令

Java虚拟机常用指令(二十二)_第20张图片

  

Java虚拟机常用指令(二十二)_第21张图片

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

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

Java虚拟机常用指令(二十二)_第22张图片

Java虚拟机常用指令(二十二)_第23张图片

为了支持Stringswitch操作,在字节码第三行调用了字符串的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指令是不同的,如左下的图;

  

Java虚拟机常用指令(二十二)_第24张图片

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

  

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

 

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

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

Java虚拟机常用指令(二十二)_第25张图片

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

  • 同步控制
    • Java虚拟机提供了monitorenter,monitorexit来完成临界区的进入和离开。达到多线程的同步;
      • 当一个线程进入同步快时,它使用monitroenter指令请求进入,如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块;
      • 当线程退出同步块时,需要使用monitorexit申明退出。
      • java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态;
    • monitorenter,monitorexit在执行时,都需要在操作数栈顶压入对象,之后,monitorenter,monitorexit的锁定和释放都是针对这个对象的监视器
      • 图示:当线程4离开临界区后,线程123才有可能进入
        • Java虚拟机常用指令(二十二)_第26张图片
    • 示例:monitorenter,monitorexit使用示例

Java虚拟机常用指令(二十二)_第27张图片

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

Java虚拟机常用指令(二十二)_第28张图片

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

add2()的字节码如下:

Java虚拟机常用指令(二十二)_第29张图片

Java虚拟机常用指令(二十二)_第30张图片

该段字节码的解析如下:
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虚拟机,java,虚拟机)