dsadJava虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
dsad由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。
dsasadsdasdassadasdsadsdsadadsaddsasdasdaasdasdassadasdasdasdsadasdasdsdadsadsadasdad——《深入理解Java虚拟机》
dsad 因此我们可以粗略认为字节码指令就由操作码组成,长度为一个字节。
⒈字节码指令集的操作码总数一共有多少个呢?
dsad 由于JVM操作码的长度为1个字节,所以指令集的操作码总数≤256条。
dsa d意味着JVM在处理超过一个字节的数据时,不得不在运行时从字节中重建出具体的数据结构,比如要将一个16位长度的无符号整数使用两个两个无符号字节(byte1 、byte2)存储起来:可以表示成 (byte1 << 8) | byte2;
【注】:因为Java虚拟机的操作码长度只有一字节,所以我们不可能让指令集相互独立,其实也就是说不可能让每一种数据类型和每一种操作都有对应的操作码指令。 比如在某种情况下,我们可以通过一些指令将一些不支持的数据类型转换成为可被支持的数据类型。
⒉以这种方式解释执行(操作数放在操作数栈中,只使用操作码)字节码有什么优缺点呢?
dsad缺点:解释执行字节码要损失一些性能。
dsad优点:我们不用担心操作数长度对其问题,毕竟只用操作码表示字节码操作,并且长度都是一个字节。当然凡事无绝对,虽然字节码指令流基本上都是单字节对齐的,但是有“tableswitch”和“lookupswitch”两条指令例外,由于它们的操作数比较特殊,是以4字节为界划分开的,所以这两条指令需要预留出相应的空位填充来实现对齐。
dsd 它的基本执行模型(不考虑异常)如下:
do {
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流长度 > 0);
⒊字节码包含数据类型信息吗?
dsad①、在JVM的指令集中,大多数指令都包含了其操作的数据类型信息。比如i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference
dsad②、也有一些指令的助记符中没有明确指明操作类型的字母,例如arraylength指令,但它的操作数只能是一个数组类型的对象;
dsad③、还有另外一些指令,例如无条件跳转指令goto则是与数据类型无关的指令。
sadadasadsadasdsasdas接下来我们通过一张表来看一下虚拟机指令集所支持的数据类型
我们通过表可以发现:
dsad①、大部分指令都不支持byte,char,short,并且没有任何指令支持boolean类型。
dsad②、编译器会将 byte和 short类型的数据 带符号扩展成为相应的int类型数据;
dsad③、将 boolean和 char类型数据 零位扩展为相应的int类型数据。
dsad④、对于②和③,相应的数组类型也是如此;
sadadasadsadasdsasdas接下来我们将字节码大致分为9类分别介绍。
①、加载和存储指令
- 作用域:将数据在栈帧中的局部变量和操作数栈之间来回传输。
- 将一个局部变量加载到操作数栈:(局部变量表–>操作数栈)
123123iload、iload_< n >、lload、lload_< n >、fload、fload_< n >、dload、dload_< n >、aload、aload_< n >
123123iload_< n >指的是:代表了一组指令集合:比如iload_0、iload_1和iload_2的集合,它们都是带有一个操作数的通用指令。而对于iload省略了显式的操作数,不需要进行操作数取操作数的动作(操作数隐藏在指令中)。 - 将一个数值从操作数栈存储到局部变量表:(操作数栈–>局部变量表)
123123istore、istore_< n >、lstore、lstore_< n >、fstore、fstore_< n >、dstore、dstore_ 、astore、astore_< n > - 将一个常量加载到操作数栈:(常量–>操作数栈)
123123bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_< i >、lconst_< l >、fconst_< f >、dconst_< d > - 扩充局部变量表的访问索引的指令: wide
- 存储数据的操作数栈和局部变量表主要由加载和存储指令进行操作;
②、运算指令
- 算数指令可以分为两种:Ⅰ、对整数型数据进行运算 Ⅱ、对浮点型数据进行运算
- 算术指令是将操作数栈上的值进行某种运算,然后把运算结果重新存入到操作数栈顶;
- 注意算数指令不直接支持byte,short,char,boolean类型的算数指令,不过可以将它们用int类型替代;
- 加法指令: 12asdasdsadsadasdasdsadasdsadasd3123减法指令:
123123iadd、ladd、fadd、dadd12sadsadasdasdsadsds3123isub、lsub、fsub、dsub - 乘法指令: 12asdasdsadsadasdasdsadasdsadasd3123除法指令:
123123imul、lmul、fmul、dmul123asdasdsadasdasdasd123idiv、ldiv、fdiv、ddiv - 求余指令: 12asdasdsadsadasdasdsada dsadasd3123 取反指令:
123123irem、lrem、frem、drem12312sadasdsadsadsadass3ineg、lneg、fneg、dneg - 位移指令:12asdasdsadsadasdasdsadas dsadasd3123按位”或”指令:
·123123ishl、ishr、iushr、lshl、lshr、lushr1231asdasdad23ior、lor - 按位”与”指令:12asdasdsadsadasdasdasdsadasd3123按位”异或”指令:
·123123iand、land 1231asdassadasdsadsadasdasddsadad23 ixor、lxor - 局部变量自增指令:12asdasdsadsadasas dsadasd3123比较指令:
·123123 iinc ·12asdasdsaasdasdsadasdsadsadasdsadsad3123dcmpg、dcmpl、fcmpg、fcmpl、lcmp - 只有除法指令和求余指令中出现除数为零时会抛出ArithmeticException异常,其余任何整型数运算场景都不抛出运行时异常。浮点数运算,也不会抛出任何运算时异常。
- JVM遵守“非正规浮点数值” 和“逐级下溢” :比如我们在进行浮点数运算时,所有的运算都必须舍入到适当的精度,如果有两种可表示的形式与该值一样接近,那将优先选择最低有效位为0的(也就是选择精确度更高的)。我们可以把其称为”舍入模式“,如果我们将浮点数近似为整数型,我们称为”向零舍入模式“。
- ·当发生数据溢出时,将会使用有符号的无穷大来表示;如果某个操作结果没有明确的数学定义的话,将会使用NaN(Not a Number)值来表示;
- 当对long类型数据进行比较时,JVM采用带符号的比较方法;而对浮点数进行比较时,JVM采用无信号比较方法。
③ 、类型转换指令
- 将两种不同的数值类型相互转换;
- JVM直接支持宽化类型转换:(小范围类型–>大范围类型)
·123123 int类型—>long、float或double类型;
·123 123long类型—>float、double类型;
·123 123float类型—>double类型; - JVM不直接支持窄化类型转换(必须显式地使用转换指令来完成):(大范围类型–>小范围类型)(转换过程很可能到值数据值的精度丢失)
·123在将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单丢弃除最低位N字节以外的内容,而N是类型T的数据类型长度,这样操作可能导致转换结果与输入值有不同的正负号。
·123窄化类型转换规则:
·1asd23Ⅰ、如果浮点值是NaN,那转换结果就是int或long类型的0。
·1asd23Ⅱ、如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v。如果v在目标类型T(int或long)的表示范围之类,那转换结果就是v;否则,将根据v的符号,转换为T所能表示的最大或者最小正数。
·1asd23Ⅲ、double—>float转换时,如果换算后的绝对值太小而不能使用float来表示的时候,就返回float类型的正负0,如果换算后的值太大,将返回float类型的正负无穷大。而double的NAN值将转换为float的NAN值。
·1asd23Ⅳ、窄化转换指令发生的精度丢失以及溢出都不会导致JVM抛出运行时异常。
④ 、对象创建与访问指令
- 虽然类实例和数组都是对象,但JVM对类实例和数组的创建与操作使用了不同的字节码指令。
- 创建类实例的指令:new
- 创建数组的指令:
1231sadsad23newarray、anewarray、multianewarray - 访问类字段(static字段)和实例字段(非static字段)的指令:
1231sadsad23getfield、putfield、getstatic、putstatic - 把一个数组元素加载到操作数栈的指令:
1231sadsad23baload、caload、saload、iaload、laload、faload、daload、aaload - 将一个操作数栈的值储存到数组元素中的指令:
1231sadsad23 bastore、castore、sastore、iastore、fastore、dastore、aastore - 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
⑤ 、操作数栈管理指令
- 直接操作操作数栈的指令:
- 将操作数栈的栈顶一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:
1231sadsad23dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 - 将栈最顶端的两个数值互换: swap
⑥ 、控制转移指令
- 让JVM有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。
- 条件分支:
1231sa23ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne - 复合条件分支:
1231sa23tableswitch、lookupswitch - 无条件分支:
1231sa23goto、goto_w、jsr、jsr_w、ret - 为了可以无须明显标识一个数据的值是否null,也有专门的指令用来检测null值。
- 对于boolean类型、byte类型、char类型和short类型的条件分支比较操作,都使用int类型的比较指令来完成。而对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp);
【注】:由于各种类型的比较最终都会转化为int类型的比较操作,int类型比较是否方便、完善就显得尤为重要,而JVM提供的int类型的条件分支指令是最为丰富、强大的。
⑦、方法调用和返回指令
- invokevirtual指令: 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派);
- invokeinterface指令: 用于调用接口方法;
- invokespecial指令: 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法;
- invokestatic指令: 用于调用类静态方法(static方法);
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
【总结】: 前面四条指令的分派逻辑都固定在JVM内部,用户改变不了,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 方法调用指令与数据类型无关。 - 方法返回指令:
1231sa23包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
⑧、异常处理指令
- 抛出异常指令:athrow
- 除了用throw语句显式抛出异常的情况之外,《Java虚拟机规范》还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出
- 注意:在JVM中,处理异常(catch语句)不是由字节码(原来由jsr,ret指令实现)来实现的,而是异常表来实现的。
⑨、同步指令
- JVM支持方法级的同步和方法内部一段代码的同步,它们都是使用管程(Monitor,也可以称为”锁”) 来实现的。
- 方法级的同步是 隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以通过常量池中的方法表结构中的 ACC_SYNCHRONIZED访问标志得知该方法是否是同步的:
1a23如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放 管程。
1a23在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。 - Java虚拟机的指令集中有 monitorenter和 monitorexit两条指令来支持 synchronized关键字的语义。
- 我们看一段字节码:
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
编译后,这段代码生成的字节码序列如下:
Method void onlyMe(Foo)
0 aload_1 // 将对象f入栈
1 dup // 复制栈顶元素(即f的引用)
2 astore_2 // 将栈顶元素存储到局部变量表变量槽 2中
3 monitorenter 💦💦以栈定元素(即f)作为锁,开始同步💦💦
4 aload_0 // 将局部变量槽 0(即this指针)的元素入栈
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // 将局部变量Slow 2的元素(即f)入栈
9 monitorexit 💦💦退出同步💦💦
10 goto 18 // 方法正常结束,跳转到18返回
13 astore_3 // 从这步开始是异常路径,见下面异常表的Taget 13
14 aload_2 // 将局部变量Slow 2的元素(即f)入栈
15 monitorexit 💦💦退出同步💦💦
16 aload_3 // 将局部变量Slow 3的元素(即异常对象)入栈
17 athrow // 把异常对象重新抛出给onlyMe()方法的调用者
18 return // 方法正常返回
Exception table:
FromTo Target Type
4 10 13 any
13 16 13 any
- 注意:
a23Ⅰ、方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。
a23Ⅱ、我们为了保证程序出现异常时,仍能使monitorenter指令和monitorexit指令正确配对执行,片一起会自动产生一个异常处理程序,它的目的就是用来执行monitorexit指令。
dsadClass文件格式以及字节码指令集。这些内容与硬件、操作系统和具体的Java虚拟机实现之间是完全独立的,虚拟机实现者可能更愿意把它们看作程序在各种Java平台实现之间互相安全地交互的手段。
white>dsasadsdasdassadasdsadsdsadadsaddsasdasdaasdasdassadasdasdasdsadasdasdsdadsadsadasdad——《Java虚拟机规范》
dsad学习思路:在满足《Java虚拟机规范》的约束下对具体实现做出修改和优化,只要优化以后Class文件依然可以被正确读取,并且包含在其中的语义能得到完整保持,那实现者就可以选择以任何方式去实现这些语义,虚拟机在后台如何处理Class文件完全是实现者自己的事情,所以只要它在外部接口上看起来与规范描述的一致即可(也有例外)。
white>dsasadsdasdassadasdsadsdsadadsasadasdddsasdasdaasddasdasdasdsadasdsdadsadsadasdad——《深入理解Java虚拟机》
【注】:比如调试器(Debugger)、性能监视器(Profiler)和即时编译器(Just-In-Time Code Generator)等都可能需要访问一些通常被认为是“虚拟机后台”的元素。
虚拟机实现的方式主要有以下两种:
a23⒈将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集(通过字节码);
a23⒉将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(通过JIT技术)。
最后:
dsadClass文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。
感谢各位的暴击三连~
今天的文章简述对JVM的理解_java执行字节码文件的命令分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/85421.html