同步操作将从 icanci/Java-Review 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
do {
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码长度>0)
在 Java 虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload 指令用于从局部变量表中加载 int 类型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:
也有一些指令的助记符中没有明确地指明操作类型的字母,如 arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象
还有另一些指令,如无条件跳转指令 goto 则是与数据类型无关的
大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将 byte 和short 类型的数据带符号扩展(Sign-Extend)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据。与之类似,在处理 boolean、byte、short 和 char 类型的数组时,也会转换为使用对应的 init 类型的字节码指令来处理。因此,大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的 int 类型作为运算类型
由于完全介绍和学习这些指令需要花费大量时间,为了让能够更快地熟悉和了解这些基本指令,这里将 JVM 中的字节码指令集按用途大致分成9类:
在做值相关操作时:
作用:加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
常用指令
xload、xload_
(其中 x 为 i、l、f、d、a,n 为 0 到 3
);xaload、xaload
(其 x 为 i、l、f、d、a、b、c、s,n 为 0 到 3
)bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、iconst_、fconst_、dconst_
xstore、xstore_
(其中 x 为 i、l、f、d、a,n 为 0 到 3
); xastore
(其中 x 为 i、l、f、d、a、b、c、s
)wide
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_
)。这些指令助记符实际上代表了一组指令(例如iload_
代表了iload_0、iload_1、iload_2和iload_3
这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload
)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中
除此之外,它们的语义与原生的通用指令完全一致(例如 iload_0
的语义与操作数为0时的iload
指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,代表非负的整数,
代表是 int 类型数据,代表 long 类型,
代表 float 类型,``代表 double 类型
操作 byte、char、short 和 boolean 类型数据时,经常用 int 类型的指令来表示
操作数栈(Operand Stacks)
我们知道,Java 字节码是 Java 虚拟机所使用的指令集。因此,它与 Java 虚拟机基于栈的计算模型是密不可分的
在解释执行过程中,每当为 Java 方法分配栈帧时,Java 虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果
具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中
局部变量表(Local Variables)
Java 方法栈帧的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中
实际上,Java 虚拟机将局部变量区当成一个数组,依次存放 this 指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。
和操作数栈一样,long 类型以及 double 类型的值将占据两个单元,其余类型仅占据一个单元
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "Hello, World"
}
}
xload_
(x
为i、l、f、d、a,n为 0 到 3
)xload
(x
为i、l、f、d、a
)x
的取值表示数据类型xload_n
表示将第n
个局部变量压入操作数栈,比如iload_1、fload_0、aload_0
等指令。其中aload_n
表示将一个对象引用压栈xload
通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload
等public class LoadAndStoreTest {
// 局部变量压栈指令
public void load(int num, Object obj, long count, boolean flag, short[] arr) {
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}
}
iconst_(i从-1到5)、lconst_(l从0到1)、fconst_(f从0到2)、dconst_(d从0到1)、aconst_null
// 演示代码
public class LoadAndStoreTest {
// 局部变量压栈指令
public void load(int num, Object obj, long count, boolean flag, short[] arr) {
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}
// 常量入栈指令
public void pushConstLdc() {
int a = 5;
int b = 6;
int c = 127;
int d = 128;
int e = 1234567;
}
public void constLdc() {
long a1 = 1;
long a2 = 2;
float b1 = 2;
float b2 = 3;
double c1 = 1;
double c2 = 2;
Date d = null;
}
// 出栈装入局部变量表指令
public void store(int k, double d) {
int m = k + 2;
int l = 12;
String str = "atguigu";
float f = 10.0F;
d = 10;
}
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "hello world";
}
}
}
作用:算术指令用于对两个操作数栈上的值进行某种特定的运算,并把结果重新压入操作数栈
分类:大体上算术指令可以分为两种:对 整型数据 进行运算的指令与对 浮点类型数据 进行运算的指令
byte、short、char和boolean类型说明
运算时的溢出
运算模式
NaN的使用
所有的运算符指令包括
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem(remainder:余数)
取反指令:ineg、lneg、fneg、dneg(negation:取反)
自增指令:iinc
位运算指令,又可分为:
比较指令:dcmpg、dcmlp、fcmpg、fcmpl、lcmp
关于++操作的理解
public class IAdd {
public void m1() {
int i = 10;
i++;
}
public void m2() {
int i = 10;
++i;
}
public void m3() {
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
}
public void m4() {
int i = 10;
i = i++;
System.out.println(i);
}
}
比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈
比较指令有:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
对于 double 和 float 类型的数字,由于 NaN 的存在,各有两个版本的比较指令,以 float 为例,有 fcmpg 和 fcmpl 两个指令,它们的区别在于在数字比较时,若遇到 NaN 值,处理结果不同
指令 dcmpl 和 dcmpg 也是类似的,根据其命名可以推测其含义,在此不再赘述
指令 lcmp 针对 long 型整数,由于 long 型整数没有 NaN 值,故无需准备两套指令
举例:
指令 fcmpg 和 fcmpl 都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为 v2, 栈顶顺位第2位元素为 v1,若 v1 = v2,则压入0;若 v1 > v2 则压入1;若 v1 < v2 则压入-1
两个指令的不同之处在于,如果遇到 NaN 值,fcmpg 会压入1,而 fcmpl 会压入-1
数值类型的数据才可以谈大小,boolean、引用数据类型不能比较大小
转换规则
精度损失问题
补充说明
当将一个浮点值窄化转换为整数类型T(T限于int类型或者long类型之一)的时候,将遵循以下转换原则
当将一个double类型窄化转换为float类型的时候,将遵循以下转换规则,通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断。
如果转换结果的绝对值太小而无法使用float表示,将返回float类型的正负0
如果转换结果的绝对值太大而无法使用float表示,将返回float类型的正负无穷大
对于double类型的NaN值将按规定转换为float类型的NaN值
对象创建之后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
举例:
public void sayHello(){
System.out.println("hello");
}
0 getstatic #8 <java/lang/System.out>
3 ldc #9 <Hello>
5 invokevirtual #10 <java/io/PrintStream.println>
8 return
数组操作指令主要有:xastore和xaload指令。具体为:
说明
方法结束调用之前,需要进行返回。方法返回指令是 根据返回值的类型区分的
举例:
通过 ireturn 指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃
如果当前返回的是 synchronized 方法,那么还会执行一个隐含的 monitorexit 指令,退出临界区
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令
这类指令包括以下内容:
将一个或两个元素从栈顶弹出,并且直接废弃:pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
将栈最顶端的两个Solt数值的位置交换:swap。Java虚拟机没有提供交换2个64位数据类型(long、double)数值的指令
指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。
这些指令属于通用型,堆栈的压入或者弹出无需指明数据类型
说明
不带 _x 的指令是复制栈顶数据并压入栈顶。包括两个指令,dup 和 dup2,dup 的系数代表要复制的 Slot 个数
带 _x 的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1、dup2_x1、dup_x2、dup2_x2。对于带 _x 的复制插入指令,只要将指令的 dup 和 x 的系数相加,结果即为需要插入的位置。因此
pop:将栈顶的1个 Slot 数值出栈。例如1个 short 类型数值
pop2:将栈顶的2个 Slot 数值出栈。例如1个 double 类型数值,或者2个 int 类型数值
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转
条件跳转指令有:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的 offset)
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置
具体说明
从助记符上看,两者都是 switch 语句的实现,它们的区别:
指令 tableswitch 的示意图如下图所示。由于 tableswitch 的 case 值是连续的,因此只需要记录最低值和最高值,以及每一项对应的 offset 偏移量,根据给定的 index 值通过简单的计算即可直接定位到 offset
方法级的同步:是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法
当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置
举例:
private int i = 0;
public synchronized void add() {
i++;
}
0 aload_0
1 dup
2 getdield #2 <com/atguigu/java1/SynchronizedTest.i>
5 iconst_1
6 iadd
7 putfield #2 <com/atguigu/java1/SynchronizedTest.i>
10 return
同步一段指令集序列:通常是由 Java 中的 synchronized 语句块来表示的。JVM 的指令集有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义
当一个线程进入同步代码块时,它使用 monitorenter 指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,知道对象的监视器计数器为0,才会被允许进入同步块
当线程退出同步块时,需要使用 monitorexit 声明退出。在 Java 虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态
指令 monitorenter 和 monitorexit 在执行时,都需要在操作数栈顶压入对象,之后 monitorenter 和 monitorexit 的锁定和释放都是针对这个对象的监视器进行的
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必须执行其对应的 monitorexit 指令,而无论这个方法是正常结束还是异常结束
为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。