浅谈 JVM 3:指令集及其执行
浅谈 JVM 3:指令集及其执行
在上一篇文章中,我们讨论了如何参考JVM规范解读Java字节码。但是,对于字节码方法字段Code中的JVM指令执行过程,我们留下了疑问。这次,我们将深入探讨JVM的指令集及其执行机制。
运行时区域
我们先来看一下JVM的逻辑区域划分。在JVM中,主要划分为5个区域:
- 方法区:存储类的结构信息,包括运行时常量池、字段和方法数据、类和接口的继承关系等。
- Java堆:用于存放对象实例,是所有线程共享的内存区域。
- 程序计数器:每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令的位置。
- Java栈:每个线程都有一个私有的Java栈,用于存储栈帧。栈帧包含局部变量表、操作数栈、动态连接和返回值等信息。
- 本地方法栈:与Java栈类似,但用于支持本地方法(即用C++等语言编写的native方法)的执行。
在执行字节码前,虚拟机需要先将.class
文件加载进来。相关类的信息会存放在方法区中。Code字段的信息就会放在此区域,后续配合Java方法栈执行。
当我们通过new Object()
或反射等方式创建对象时,JVM会在Java堆中为对象分配内存。方法区和Java堆为线程公有内存区域,所以当我们多线程访问同一个对象时,需要进行加锁操作,来处理线程并发处理数据所引发的问题。锁操作的实现,也和指令集相关。
其他三个区域(程序计数器、Java栈、本地方法栈)均为线程私有区域。
程序计数器
程序计数器保存当前线程中,当前执行的字节码指令位置。
Java方法栈
Java方法栈为栈型数据结构,具有后进先出的特性。其组成元素为栈帧。每当执行一个方法时,会根据该方法对应的Code属性生成对应栈帧,压入到栈中。方法执行完毕时,栈帧出栈。当前正在执行的方法处在栈顶的位置。
本地方法栈
本地方法栈和Java方法栈类似,不过它处理的是C++编写的native方法。
栈帧结构
每个栈帧包括局部变量表、操作数栈、动态连接、返回值等内容。动态连接为指向运行时常量池中该栈帧所属方法的引用,目的是为支持方法调用的动态连接。动态连接是Java支持动态特性的关键所在,此部分我们之后专门花篇幅介绍,此处先浅尝辄止。返回值则代表当前方法计算结果,返回给上一级调用方法。
局部变量表
局部变量表用于存放方法参数和内部定义的局部变量。在字节码中方法的Code属性定义了max_locals属性表示局部变量表的最大容量。局部变量表以变量槽为单位存储数据。其实际占用内存空间大小虚拟机规范并未规定,可由虚拟机各自实现。只要求一个变量槽可存储下boolean、byte、char、short、int、float等类型数据。所以对于double、long类型数据会占用两个变量槽的空间。boolean在此被使用0、1存储。由此可以看出,JVM中的类型和Java代码中的类型并不一致。
操作数栈
操作数栈也是一个后进先出栈,其最大深度同样在Code中有所体现。这块区域供字节码指令调用时使用,保存指令调用所需参数和结果。
运行时栈帧结构和指令集
对于JVM指令集,可以参考官方文档。在此,我们结合一段简单代码,了解一下常见指令集。
Java代码如下:
private int getThisIsInt() {
int a = 2;
return mThisIsInt * a;
}
编译后字节码如下:
0 iconst_2
1 istore_1
2 aload_0
3 getfield #7 <Demo.mThisIsInt : I>
6 iload_1
7 imul
8 ireturn
iconst_2
:将int类型常量2压栈到操作数栈中istore_1
:将操作数栈的int值出栈,放入局部变量表槽位1的地方aload_0
:加载局部变量表槽位0处的引用到操作数栈中,此处为当前对象的引用objectrefgetfield #7
:获取当前类的运行时常量池#7变量,此指令执行完后,objectref出栈,获取到的值入栈iload_1
:从局部变量表槽位1处加载值到操作数栈中imul
:将操作数栈中靠近栈顶的两个值出栈,做乘法,将结果压栈ireturn
:将当前方法操作数栈中的值出栈,并压栈到调用者方法的操作数栈中
可对照下图梳理指令执行时,局部变量表和操作数栈的变化情况。
JVM指令集较多,限于篇幅无法一一介绍。但其遵循一定规律,如下:
- Tload_X:将局部变量表数据加载到操作数栈中,T为类型,如iload为加载int类型,fload加载float类型。X为局部变量表槽位索引
- Tstore_X:将操作数栈数据出栈,存入局部变量表。T、X和上文含义一致
- Tconst_X:将T类型常量X压入操作数栈
- Tmul:将操作数栈的两个T类型常量相乘,结果放入操作数栈。类似的有除法Tdiv、加法Tadd等
- i2b:int类型转换为byte数据,类似指令有i2c、f2i等
- new:对象创建指令
- ifeq:分支控制指令,类似指令有iflt、ifnull、goto等
- invokevirtual:方法调用指令,类似指令有invokespecial、invokestatic等
- monitorenter:同步指令,有加锁操作时会添加此指令,对应monitorexit代表退出同步
更加详细的指令介绍,请参考官方文档。
总结
本次我们了解了JVM运行时区域划分,指令集运行时所依赖的局部变量表和操作数栈等结构,和指令集的类型和操作影响。
指令集中包含了锁操作对应的monitorenter
、monitorexit
指令,和方法调用对应的invokevirtual
等指令。这些指令和JVM实现线程安全和方法的动态连接密切关联。线程安全在我们日常开发中至关重要。方法的继承和重写又是面向对象语言的重要特征。所以,在JVM层面,这些都是如何实现的呢?我们下次有时间再聊。