深入理解JVM - 字节码指令
前言
字节码指令的部分更多要和实战搭配学习和使用,所以这一节将会是简单概述字节码的相关指令内容,和class结构不同,字节码指令常见的命令是需要了解的,虽然我们很多时候并不需要研究底层字节码的指令,但是譬如动态语言的支持就是通过新增字节码指令完成的。
这一节内容更加建议配合阅读字节码,根据字节码阅读来加强记忆。后续的文章将会单独开一篇讲一讲字节码的源代码处理。
概述
- 了解jvm字节码指令的基本特点
- 了解jvm的常见字节码指令(建议在阅读字节码的时候不知道什么意思的时候再来查)
- 部分指令的处理细节讲解。
- 最后根据书中的内容整理了一个excel表格,供阅读的时候进行快速查阅
字节码指令
简介
java的指令包含特定语意操作数字(操作码)并且占一个字节长度的,同时java的指令并不是面向寄存器架构的,而是采用操作数栈(栈帧)的架构。由于一个字节码的长度被限制为i一个字节,所以意味着操作数字并不能够超过256条(1111 1111)。如果超过长度的上限,就需要根据位操作构建一个新的的数据结构,也可以理解为向上升级,比如byte1升级为byte2就可以如此表示:(byte << 8) | byte2
。虽然这种操作会损失一定的性能,但是换来的是可以省略大量的填充占位符来实现对齐操作。
只有tableswitch 和 lookupswitch 这两条指令例外,需要预留空位来进行对齐填充
支持数据类型
下面是java支持的数据类型表,这里可以看到关于Byte,short,boolean等操作在表上没有的,并不是说不支持,而是jvm处理的时候底层将他们统一作为int操作了,所以只需要按照int的操作理解即可。
指令介绍
由于指令的内容比较多,这里同样根据书中给的图表作为笔记。
加载指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,关于局部变量表和操作数栈的内容在之前的文章中都是讲的十分笼统的,简单理解为虚拟机栈的栈帧基础结构中的内容,关于栈桢的具体结构会放到后续的文章讲解。
运算指令
运算指令就是我们常见的数学操作,包括加减乘除以及位移等。这里值得关注的是关于指令的操作 溢出,jvm是如何处理的:
- 如果除数或者余数是0,则会抛出ArithmeticException异常(运行时异常)
- 如果数据是整型或者浮点型,进行指令计算的时候如果发生溢出都不会出现或者抛出异常
- 整型的溢出会出现一个不稳定的随机数,并没有规定溢出后为哪一个数字,有可能正数也可能负数。
- 浮点操作由于存在精度,因此如果出现非精确的结果会出现“逐级下溢”的特性,意思就是小数点的部分会舍弃掉,并且舍入到一个不大于原数到最接近到值。同样,由于精度的存在,无限小数部分会根据精度进行摄入,比如常见的
10/3
的操作结果。 - 浮点运算严格按照IEEE 754的规范。
类型转换指令
该指令用于尝试将两个类型进行互转,下面是java支持的宽化类型转化(小类型转大类型):
- int转long、float、double
- long类型转 float和double
- float转double
窄化的范围转化稍微复杂一些,他遵循如下的规则:
- 如果是整型类型,将会直接丢弃位数获得小类型的对应位数,这会导致数字可能变为一个负数(计算机按照最高位为0或者1判断数字的正负值)
- 如果是浮点型,则遵循如下的规则
- 如果浮点值是一个NAN,那么转换之后还是NAN
- 如果向下转型之后,如果不是无穷大,则按照IEEE 754的向零舍入模式取整,获得整数值v,如果v在其数据范围的允许范围之内则不变,否则按照向下转型所能表示的最大值(或最小值)表示。
- 如果Double向下转型无法被float浮点值,则按照float的正负值0进行表示。如果转化绝对值太大,则根据float的最大值表示。对于double类型的NaN值将按规定转换为float类型的NaN值
对象创建与访问指令
这里只需要注意创建实例和创建数组使用了不同的命令即可。
操作数栈的指令
操作数栈的指令指的是堆栈之中操作的指令
控制指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下 一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存 器的值。这里需要注意的内容是Java区分了引用类型和基础类型的比较操作,并且有不同的指令进行区分,另外,之前提到过的boolean、byte、short 等指令是通过int类型进行比较的
方法调用和返回指令(重点):
这一块是重点内容,也是有可能会被问到的点,当然具体的实操在后续的文章讲解:
invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派), 这也是Java语言中最常见的方法分派方式。 invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找 出适合的方法进行调用。 invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和 父类方法。 invokestatic指令:用于调用类静态方法(static方法)。 invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面 四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedy namic指令的分派逻辑 是由用户所设定的引导方法决定的。
invokedynamic指令是这一块内容的重点,至于方法分派的内容,同样会在后续的文章进行讲解。
最后,方法的返回指令是根据数据类型确定的,比如int、double、float的返回类型指令是不一样的。这里也要再次说明一下,byte、boolean、short这些数据类型的返回值同样是看作int处理的。(可以看到int的重要性了)
异常处理指令
这里又一个考点,需要重点记忆:而在Java虚拟机中,处理异常(cat ch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和 ret指令来实现,现在已经不用了),而是采用异常表来完成。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中 有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字 需要Javac编译器与Java虚拟机两者共同协作支持。
这里给出了具体的一段代码示例:
可查字节码指令表
这里个人根据书中的内容整理了一份字节码指令的查阅表,用了石墨文档方便随时查阅。
地址:shimo.im/sheets/GyrH… 《字节码指令》
总结
字节码指令的重点是方法的调用相关内容,在面试当中有可能会被问到JDK特性的是如何通过字节码实现的,同时有必要查看java的虚方法(就是最为常见的方法调用)是如何调用和实现的,借此也可以理解重载和重写的实现。
写在最后
这一节的内容其实并不需要背诵,理解有一个基础的印象即可,这部分也不是背的重点。
可以看到,专栏到了后面的内容多数都是从书上总结了,我们不需要深入底层,所以周大神的书多翻翻基本就够了,另外JVM更多还是要自己做笔记总结。