JVM虚拟机---(13)字节码指令

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq1021979964/article/details/98060005

一、字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的0至多个代表此操作所需参数(称为操作数,Operands)而构成,由于虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个字节码。

操作码的长度为1个字节,因此最大只有256条。

基于栈的指令集架构。

二、字节码与数据类型

在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。

如:iload(代表int)、fload(代表float)、lload(代表long)、dload(double)、aload(引用数据类型)等。

大多数指令是包含类型信息的。

也有不包含类型信息的,如arraylength是操作数组类型,goto与类型无关。

Java由基本数据类型和引用数据类型组成,而操作码是由一个字节长度组成,即最大只有256条,如果每个指令都包含类型信息,是不够的。

对此,Java提供了解决方案:类型多、指令少的解决方案。

  1. 通过转换指令,将一些不支持的类型转换成支持的类型(使用一条转换指令表示多条指令。)
  2. 字节码指令本身不支持如:char、byte、short等基本数据类型(即不包含这些基本数据类型的信息,)Java将这些都当成int类型来处理,减少了很多的指令。

三、加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。(如果将进行运算等操作需要入栈,将局部变量表中的数据入栈加载入操作数栈中,在操作数栈中运算完毕再将结果出栈存到局部变量表中)。

  1. 将一个局部变量加载到操作栈的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
  2. 将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
  3. 将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
  4. 扩充局部变量表的访问索引的指令:wide。

案例

public class Test {

    public int add(int a,int b) {
		int c = a + b;
		return 1+1;
	}

}

第一个是无参构造器的指令。

description(描述):()V为无参数,无返回值。

flags(访问标志):ACC_PUBLIC为public。

code(内容):

默认stack(栈深度)=1。

locals(局部变量表大小)=1。

args_size(参数个数)=1。

默认是有this参数的。

aload():表示引用数据类型。

invokespecial:表示方法调用实例或初始化方法。

return:表示声明为void的方法,或者是实例,初始化方法

第二个是自定义的add方法的指令。

description(描述):(II)I为两个int类型的参数,返回为int类型。

flags(访问标志):ACC_PUBLIC为public。

code(内容):

默认stack(栈深度)=2。

locals(局部变量表大小)=4。

args_size(参数个数)=3。

默认是有this参数的。

iload_1:表示int数据类型。

iload_2:表示int数据类型。

iadd:加法运算指令,表示前面两个相加。

istore_3:表示将一个数值从操作数栈存储到局部变量表。

iconst_2:表示将一个常量加载到操作数栈

ireturn:表示返回值是boolean、byte、char、short和int类型时使用。

四、运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作栈顶。

算术指令大体分为两种:整型数据运算指令、浮点数据运算指令。

       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。

五、类型转换指令

类型转换指令可以将不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

宽化类型转换(小范围类型向大范围类型的安全转换):

int类型到long、float或者double类型。

long类型到float、double类型。

float类型到double类型。

窄化类型转换(大范围类型向小范围类型的转换):

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。(2是to,例如:i2b就把int类型转化为byte类型)。

窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。

数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虚

拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。

案例

public class Test {

    public int add(int a,int b) {
		long c = a;
		long d = 10;
		int e = (int)d;
		return 1+1;
	}

}

add方法的指令,只说转换指令。不说别的了。

i2l:表示int类型转换成long类型。(宽化转换,没有问题)

l2i:表是long类型转换成int类型。(窄化转换,如果数值范围超过int的长度,就会出现异常,如果是float类型转换成int,long等类型,会丢失精准度)

宽化类型转换处理

比如:

User user = getUser();

Object obj = user;

窄化类型转换处理

比如:

User user = getUser();

Object obj = user;

User u = (User) obj;

六、对象创建和访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。指令如下:

  1. 创建类实例的指令:new。
  2. 创建数组的指令:newarray、anewarray、multianewarray。
  3. 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic。
  4. 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
  5. 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、 fastore、dastore、aastore。
  6. 取数组长度的指令:arraylength。
  7. 检查类实例类型的指令:instanceof、checkcast。

案例

public class Test {
	
	public static void main(String[] args){
		User user = new User();
	}

}
class User {
	String name;
	static int age;
}

mian方法的指令

main方法是静态方法,不能有this,所以args_size实际有几个就有几个。

new:创建类实例的指令。

dup:复制栈顶一个数值并将复制值重新压入栈顶。

invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

astore:将一个数值从操作数栈存储到局部变量表。

return:表示返回值是void,或者是实例。

七、操作数栈管理指令

如果操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作数栈的指令。

  1. 将操作数栈的栈顶一个或两个元素出栈:pop、pop2.
  2. 复制栈顶一个或两个数值并将复制值或者双份的复制值重新压入栈顶:dup、dup2、 dup_x1、dup2_x1、dup_x2、dup2_x2。
  3. 将栈最顶端的两个数值互换:swap。

案例

public class Test {
	
	public static void main(String[] args){
		int a = 10;
		if(a > 10) {
			System.out.println(" > 10");
		} else {
			System.out.println(" < 10");
		}
	}
}

Main方法的指令

bipush:将一个常量10加载到操作数栈。

istore_1:将一个数值从操作数栈存储到局部变量表。

iload_1:int类型。

bipush:把常量10加载到操作数栈。

if_icmple:条件比较操作数栈。

getstatic:获取if条件中比较的数值。

idc:加载常量入栈,System.out.println()内的数据。

invokevirtual: 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分 派),这也是Java语言中最常见的方法分派方式。

getstatic:获取else。

idc:加载常量入栈,System.out.println()内的数据。

invokevirtual: 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分 派),这也是Java语言中最常见的方法分派方式。

return:返回值为void。

八、控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令如下:

  1. 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、 if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
  2. 复合条件分支:tableswitch、lookupswitch。
  3. 无条件分支:goto、goto_w、jsr、jsr_w、ret。

九、方法调用和返回指令

方法调用指令

  1. invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分 派),这也是Java语言中最常见的方法分派方式。
  2. invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对 象,找出适合的方法进行调用。
  3. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  4. invokestatic指令用于调用类方法(static方法)。
  5. invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻 辑是由用户所设定的引导方法决定的。

方法返回指令

ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和 areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

方法调用指令与数据类型无关,方法返回指令是根据返回值的类型区分。

十、异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

    使用try…catch会影响程序的性能。

案例

public class Test {
	
	public static void main(String[] args){
		throw new RuntimeException("exception ..");
	}
}

main方法的指令

new:创建RuntimeException对象。

dup:复制栈顶一个数值并将复制值重新压入栈顶。

idc:加载常量入栈。

invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

athrow:显式抛出异常。

十一、同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都 是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作 之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。

当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZE访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法最后当方法完成(无论是正常完成还是非正常完成)时释放管程。

在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的 指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现 synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。

猜你喜欢

转载自blog.csdn.net/qq1021979964/article/details/98060005