十分钟拿下JVM字节码指令面试题
面试题
1.介绍一下你了解的一些字节码指令
2.Java虚拟机栈的栈帧中有什么,分别介绍一下
引言
“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力;
区别:
- 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上
- 虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
高级语言到机器语言
- Java代码经过编译由 .java代码源文件经过词法解析、语法解析、语义解析以及生成字节码最终生成 .class字节码文件
- 字节码文件是二进制文件,字节码文件由一些JVM能够识别的字节码指令
- 通过JVM的执行引擎将字节码指令解释/编译为对应平台上的本地机器指令
- 即JVM中的执行引擎将将高级语言翻译为了机器语言
运行时栈帧结构
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构。
每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。字节码文件中则记录了栈帧中局部变量和操作数栈的大小。
栈帧的概念结构如下图所示:
操作数栈
操作数栈主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量
执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
【例子】
调用下面代码的fun方法,jvm执行引擎则会先将2和4进行入栈操作,然后分别将2和4出栈执行指令iadd进行加法操作,最后将结果3入栈
public int fun(){ int a = 2; int b = 4; return a+b; }
当然上面的fun方法调用过程不止是简单的在操作数栈进行入栈出栈操作,还涉及到了栈帧中另外一个结构叫做"局部变量表"
局部变量表
局部变量表用于用于存放方法参数和方法内部定义的局部变量;
换句话说,局部变量表存放的是编译期可知的各种数据类型(如byte short int等)和对象引用,所以在字节码文件中也确定了局部变量表的的最大容量。
【例子】
public int fun(){ int a = 2; int b = 4; return a+b; }
继续用上面的a+b的代码,上面说到执行fun方法的时候会借助操作数栈通过一些出栈入栈的操作来完成加法,但jvm执行引擎中还用到了局部变量表,具体步骤如下(下面的步骤由javap命令所得,javap具体操作下文会介绍):
0: iconst_2 1: istore_1 2: iconst_4 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn
iconst_2指令是将int型2推送至栈顶
istore_1指令将栈顶int型数值2存入局部变量表中的第1个变量槽中
iconst_4指令将int型4推送至栈顶
istore_2指令将栈顶int型数值4存入局部变量表中的第2个变量槽中
iload_1指令的作用是将局部变量表第1个变量槽中的整型值复制到操作数栈顶
iload_2指令的作用是将局部变量表第1个变量槽中的整型值复制到操作数栈顶
iadd指令将操作数栈的栈顶两个元素出栈,进行相加,将结果 6 重新入栈
也许会觉得上面这样把数从操作数栈放到局部变量表又从局部变量表复制到操作数栈是不是有点多余,其实一点也不多余,如下代码所示,在第四行新增了一个加法操作,这样应该就能够看出局部变量表的作用了
public int fun(){ int a = 2; int b = 4; int c = a*b; return b+c; }
像a和b变量如果只存在于操作数栈,那么执行了乘法操作之后,再次想执行b+c的时候,我们的操作数栈已经没有变量b了,所以需要一个局部变量表,存储变量以及运算过程产生的中间变量
操作数栈的相关指令如下:
其他的基本数据类型的加载和存储的指令如下:
刚刚的字节码执行执行到最后,还有一个ireturn指令
ireturn指令是方法返回指令之一他将结束方法,并将操作数栈的栈顶元素返回给方法的调用者
动态连接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在class文件的常量池里
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。 另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)例如上面fun方法就是通过ireturn指令结束方法调用并返回值。
相关的返回字节码指令如下:
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继 续执行。
- 方法正常退出时,主调方法的PC计数器的值就可以作为返回地址
- 方法异常退出时,返回地址是要通过异常处理器表来确定的
Javac和Javap查看字节码指令
javac即java compiler,能够将源代码编译为字节码文件
javap是JDK自带的反汇编器,可以查看java编译器生成的字节码
在Java项目下新建一个Test类,代码如下
public class Test { public int fun(){ int a = 2; int b = 4; return a+b; } public static void main(String[] args) { Test test = new Test(); test.fun(); } }
打开控制台Terminal,将地址切换到Test.java所在目录
执行javac Test.java将Test编译为class文件,然后执行javap -v Test.class,查看字节码指令
找到对应fun方法下对应的字节码指令
这样就得到了字节码指令
在fun字节码指令上面,会发现invokespecial指令,用于调用实例构造器<init>()方法、私有方法和父类中的方法
其它方法调用指令:
invokestatic。用于调用静态方法。
invokespecial。用于调用实例构造器<init>()方法、私有方法和父类中的方法。
invokevirtual。用于调用所有的虚方法。
invokeinterface。用于调用接口方***在运行时再确定一个实现该接口的对象。
invokedynamic:动态解析出需要调用的方法,然后执行
非虚方法概念:
如果方法在编译期就确定具体调用版本,这个版本在运行期间是不可变的。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其它方法都为虚方法。
但是final方法是由invokevirtual指令调用
Java字节码指令
操作数栈专用指令
基本数据类型的加载和存储指令:
运算相关指令
数组相关指令
- 新建基本类型数组 newarray
- 新建引用类型数组 anewarray
- 生成多维数组 multianewarray
- 求数组长度 arraylength
数组的加载指令以及存储指令:
总结
全文从Java虚拟运行时栈帧的结构,引出了一些常见的Java字节码指令,如操作数栈相关指令、基本数据加载和存储指令、数组相关指令以及方法调用指令,但值得注意的是想iconst、iload、iadd这些字母形式的字节码叫做”助记符“只是方便理解,在字节码文件实际存储的是十六进制。如 iadd对应存储的是 0x60。
字节码指令表:https://www.yuque.com/docs/share/b1ec3861-b3b2-40f2-9e19-a1ca22ff50ba
相关面试题:
1.介绍一下你了解的一些字节码指令
2.Java虚拟机栈的栈帧中有什么,分别介绍一下
参考:
《深入理解Java虚拟机 第三版》周志明
《深入拆解 Java 虚拟机》郑雨迪
#Java##Java面试##面试##java面试题#