当前位置: 首页 > article >正文

栈虚拟机和寄存器虚拟机,有什么不同?

本来这节内容是打算直接讲字节码指令的,但讲之前又必须得先讲指令集架构,而指令集架构又分为两种,一种是基于栈的,一种是基于寄存器的。

那不妨我们这节就单独来讲讲栈虚拟机和寄存器虚拟机,它们有什么不同,以及各自的优缺点。

栈和寄存器

关于栈这个数据结构,我们前面曾讲过,戳链接直达。

寄存器(Register)是中央处理器(CPU)内用来暂存指令、数据和地址的存储器,也是 CPU 中读写最快的存储器。

图片来源于cxuan

图片来源于cxuan

从硬件层面来说,栈位于内存当中,而寄存器位于 CPU 当中,这也是为什么,我们通常会说,基于寄存器架构的虚拟机会比基于栈的虚拟机快的原因。

基于栈的虚拟机

前面我们讲 JDK 的发展历程时,提到了 Hotspot VM,它是血缘最正统的 Java 虚拟机。

 

HotSpot VM 是基于栈的一种虚拟机,当 Java 程序运行时,HotSpot VM 加载编译后的字节码文件(也就是.class 文件),其解释器或JIT编译器会读取文件中的字节码指令,将它们解释(或编译)为机器码。

方法调用和执行过程中的数据(如局部变量和中间结果)会存储在栈(操作数栈,下面会讲)中,字节码指令操作这些数据,然后执行程序逻辑。

下面这幅图我们之前在讲JVM 是如何运行 Java 代码的时候讲过。

 

main 方法被执行的时候,JVM 会创建一个栈帧(Stack Frame),通过存储局部变量表、操作数栈、动态链接、方法出口等信息来支撑和完成方法的执行,栈帧就是虚拟机栈中的子单位。

 

栈帧本身也是一种栈结构,用于支持虚拟机进行方法调用和方法执行,遵循 LIFO 的原则,每个栈帧都包含了一个方法的运行信息,每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

图片来源于网络,作者浣熊say

图片来源于网络,作者浣熊say

虚拟机栈是线程私有的,每个线程都有自己的 Java 虚拟机栈。方法调用时都会创建一个新的栈帧,该栈帧被推入虚拟机栈,成为当前活动栈帧。

  • 入栈:方法调用时,虚拟机栈会为这个方法分配一个栈帧,这个栈帧被压入虚拟机栈,成为当前的活动栈帧。PC 寄存器指向当前栈帧的指令,执行方法的指令序列从该地址开始。
  • 出栈:方法执行完成后,对应的栈帧会被移除,控制权回到前一个栈帧,前一个栈帧中的返回值成为当前活动栈帧的一个操作数,继续执行。

其中的操作数栈(Operand Stack)也是一种栈结构,用于保存方法执行时的中间结果、参数和返回值。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。

在方法执行的过程中,操作数栈被用于执行各种字节码指令。例如,将两个数字相加的指令会从操作数栈中弹出两个数字,将它们相加,然后将结果压入操作数栈中。

另外,操作数栈的内容是临时的,它的生命周期和方法的生命周期是一样的,当方法执行结束后,操作数栈也会被销毁。

 

R 大曾在知乎的贴子里提到过:

VM 当初设计的时候非常重视代码传输和存储的开销,因为假定的应用场景是诸如手持设备、机顶盒之类的嵌入式应用,所以要代码尽量小;外加基于栈的实现更简单(无论是在源码编译器的一侧还是在虚拟机的一侧),而且主要设计者 James Gosling 的个人经验上也对这种做法非常熟悉(例如他之前实现过 PostScript 的虚拟机,也是基于栈的指令集),所以就选择了基于栈。

我们简单来看一下基于栈的虚拟机方法执行的过程,以下面的代码为例:

int a = 33;
int b = 44;
int c = a + b;

通过 javap -c Main 命令可以查看对应的字节码,如下所示:

Compiled from "Main.java"
public class com.github.paicoding.forum.test.javabetter.jvm.Main {
  public static void main(java.lang.String[]);
    Code:
       0: bipush        33
       2: istore_1
       3: bipush        44
       5: istore_2
       6: iload_1
       7: iload_2
       8: iadd
       9: istore_3
      10: return
}

我们用图来说明指令执行的过程,大致如下。

 

  • iload_0 将 33 压入操作数栈中
  • iload_1 将 44 压入操作数栈中
  • iadd 将操作数栈中的 33 和 44 弹出,相加后将结果 77 压入操作数栈中
  • istore_2 将栈顶的 77 弹出,存入局部变量表中下标为 2 的位置

关于字节码指令的具体释义,我们放到下一节去细讲,这里主要是带大家体会一下基于栈的虚拟机和基于寄存器的虚拟机之间的差别。

基于寄存器的虚拟机

那除了有基于栈的虚拟机实现,当然也有基于寄存器的虚拟机实现,比如 LuaVM,负责执行 Lua 语言,一门轻量级的脚本语言,可戳链接了解。

5.0 之前的 Lua 其实是用基于栈的指令集,到 5.0 才改为用基于寄存器的。出于两点考虑,一是减少数据移动次数,降低数据迁移带来的拷贝开销;二是减少虚拟指令条数,提高指令执行效率。

 

好,我们就基于 lua 来看一下基于寄存器的虚拟机方法执行的过程。

第一步,安装 lua,这里我用的是 macOS,直接用 brew 安装就好了。

brew install lua

Windows 用户可以查看这个文档:lua-users wiki: Building Lua In Windows For Newbies

也可以通过 Lua for Windows 来完成安装:

Releases · rjpcomputing/luaforwindows · GitHub

我们来编写一段简单的 lua 代码,保存为 example.lua。

local a = 33
local b = 44
local c = a + b

然后查看字节码指令。

luac -l example.lua

结果如下:

 

main <example.lua:0,0> (6 instructions at 0x600002144080)
0+ params, 3 slots, 1 upvalue, 3 locals, 0 constants, 0 functions

这是函数的描述,表示这是 example.lua 文件中的主函数。它包含 6 条指令。函数不接受参数(0+ params),有 3 个本地变量槽位(3 slots),1 个闭包变量(1 upvalue),3 个本地变量(3 locals),没有常量(0 constants)和内部函数(0 functions)。

接下来是具体的指令:

  1. VARARGPREP 0:准备变长参数,用于处理传入的参数。
  2. LOADI 0 33:将整数 33 加载到寄存器 0。
  3. LOADI 1 44:将整数 44 加载到寄存器 1。
  4. ADD 2 0 1:将寄存器 0 和寄存器 1 中的值相加,并将结果存放在寄存器 2。对应于脚本中两个数值的加法操作。
  5. MMBIN 0 1 6; add:这是一个元方法(metamethod)调用,用于处理加法操作。这指示 Lua 虚拟机查找并执行 add 元方法。元方法是 Lua 中用于重载标准操作符的特殊方法。
  6. RETURN 3 1 1; 0 out:返回操作,将寄存器 3 中的值作为返回值。1 1 表示从寄存器 3 返回一个值,0 out 指没有额外的返回值。

小结

基于栈的优点是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所需要的指令数也比寄存器的要多,需要频繁的入栈和出栈。

基于寄存器的优点是速度快,有利于程序运行速度的优化,但操作数需要显式指定,指令也比较长。


http://www.kler.cn/a/383924.html

相关文章:

  • Cursor的chat与composer的使用体验分享
  • 智慧商城项目-VUE2
  • 自动驾驶革命:从特斯拉到百度,谁将主宰未来交通?
  • 【万字总结】数据结构常考应用大题做法画法详解_树_哈希表_图_排序大总结
  • Spring挖掘:(AOP篇)
  • python使用短效IP池的好处是什么?
  • 【运动的&足球】足球场景目标检测系统源码&数据集全套:改进yolo11-ASF-P2
  • 如何基于pdf2image实现pdf批量转换为图片
  • leetcode动态规划(二十六)-最长重复子数组
  • JS数据结构之“栈”、“队列”、“链表”
  • 【数学】通用三阶矩阵特征向量的快速求法 超简单!!!
  • 重构代码之参数化方法
  • 这款神器,运维绝杀 !!!
  • Docker 配置镜像加速
  • ECMAScript 6
  • 台式电脑如何改ip地址:全面解析与实操指南
  • AJAX学习笔记总结
  • 【LeetCode】【算法】283. 移动零
  • 数据结构之线段树
  • LangChain实际应用
  • Java内存区域详解
  • 前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
  • tensor数组维度转化
  • Linux学习笔记之时间日期和查找和解压缩指令
  • CSP/信奥赛C++刷题训练:经典广搜例题(3):洛谷P1596 :[USACO10OCT] Lake Counting S
  • 【C++】条件变量condition_variable