KVM的执行引擎(上) — 栈和帧

接下来的两篇将介绍在KVM中字节是如何执行的,这是KVM中比较核心的内容,分为两部分来讲,本篇先介绍虚拟机中的栈和帧是如何实现的。

 

首先来看一些全局指针,在头文件kvm/vmcommon/h/interpret.h中定义有以下结构:

struct  GlobalStateStruct { 
    BYTE
*          gs_ip;  /*  Instruction pointer (program counter)  */
    cell
*          gs_sp;  /*  Execution stack pointer  */
    cell
*          gs_lp;  /*  Local variable pointer  */
    FRAME         gs_fp; 
/*  Current frame pointer  */
    CONSTANTPOOL  gs_cp; 
/*  Constant pool pointer  */
};

这五个变量就像CPU中的寄存器一样,在KVM的运行过程中起到非常基础性的作用。它们分别是程序计数器、执行栈指针、局部变量指针、当前帧指针和当前常量池指针。

Java虚拟机为每一个线程开设一个栈,栈中存储的数据以“帧”为单位,虚拟机在调用一个新的方法时,会向栈中压入一个新帧,帧内数据是这个方法的运行状态,Java字节码的执行总是在当前帧内进行,方法运行结束时这个帧会被弹出。所以这个栈可以称为“方法栈”,帧可以称为“方法帧”。

按照Java虚拟机的规范,一个帧应由三个部分组成:局部变量区,操作数栈和帧数据区。每个帧的局部变量区和操作数栈的大小都可能不一样,要依方法本身的庞大程度而定,但在调用一个方法时,可以根据这个方法的字节码计算出所需要的局部变量区和操作数栈的大小。规范对帧数据区的大小没有规定,帧数据区的大小和内容可由虚拟机实现来决定。

局部变量区:

局部变量区一般会位于帧中最前面(即地址最小)的位置,它包含了对应方法的参数和局部变量,一般情况下,它的大小是向4字节对齐的,每4字节是一个“字”,变量以“字”为单位来存入。在它的最前面顺序存放的是对应方法的参数,类型为intfloatreferencereturnAddress的参数占一个“字”,类型为byteshortchar类型的参数对被转化为int型,所以也占一个字;longdouble类型的值要占用两个字。当然,“字长”选为多少是由虚拟机实现自己来决定的,不是一定要选4字节为一个字,如果选8字节构成一个字的话,所有值都只占一个字,更加整齐,但是浪费了很多空间。

如果方法不是静态的,那么虚拟机会自动将方法所在对象的句柄存在局部变量区中索引为0的位置,真正的参数从位置1开始存;而如果方法是静态的,它就与具体的对象没有关系,所以不必存放对象句柄,参数从位置0开始存放。

在局部变量区接下来的空间中,虚拟机可以按照任意的方式来存贮方法内的局部变量。

操作数栈:

操作数栈的作用相当于CPU中的通用寄存器,由于Java虚拟机是一台虚拟的机器,它没有真正的寄存器,而Java虚拟机也没有选择与CPU相似的方式来模拟通用寄存器,而是选择了另一种方法 使用栈,Java指令所使用的操作数都从操作数栈中得到。

某方法在被调用的时候,同样可计算出它需要多大的操作数栈,所以在一个帧中,操作数栈的大小也是固定,而它的位置可以由实现来决定,不过在接下来KVM的实例中我们会发现,把操作数栈放在帧的最后面(地址最大)的地方是一个好办法。

帧数据区:

帧数据是由虚拟机实现任意设计的,通常它都被用来实现常量池解析和异常处理等等。

 

 

下面来看一看,在KVM中如何实现栈和帧。

数据结构:

在头文件kvm/vmcommon/h/frame.h中定义了栈和帧的结构:

/*  STACK  */
struct  stackStruct {
    STACK    next;
    
short     size;
    
short     xxunusedxx;  /*  must be multiple of 4 on all platforms  */
    cell     cells[STACKCHUNKSIZE];
};
typedef 
struct  stackStruct *          STACK;
 

每一个stackStruct结构体的变量就是一个Java栈或Java栈的一部分,因为每一个stackStruct结构的大小是固定的,如果不够用,可以得用next指针来扩展成链表。size是本结构体的大小,xxunusedxx是剩余空间,cells则是实际的存贮空间。

每一个线程开始的时候都会生成一个新的stackStruct,在每一次压入新帧的时候会查看剩余空间是否够用,如果不够用,还会再生成新的stackStruct

frameStruct这个结构的大小是固定的,它并不是一个帧,而只是“帧数据区”,前面说过,由于局部变量区和操作数栈的大小都不固定,所以整个帧的大小也是不固定的。帧的空间是在调用方法的时候临时计算出来的,然后在当前线程的栈中申请,frameStruct结构的指针占据其中的一个字,其余空间都给局部变量区和操作数栈用。

KVM中栈和帧的模型如下:(为理解方便,暂不考虑栈要扩展的情况)

当栈中只有一个帧时,栈的结构如图所示:

 

当栈中只有一个帧时,帧的低字节区是局部变量,接下来会有一个字(4字节)指向帧数据区结构体,再接下来的空间就是操作数栈。

只有一帧时,帧中各部分的结构很明晰,但如果多于一帧时,情况就会有些复杂,下面看当再压入一帧时的图示:

这个图或许跟想像中的不一样,两帧数据之间出现了重叠。图中画出了一条虚线,这条虚线的位置是上一帧结束的位置,但是却没有成为新的一帧开始的位置,新的一帧在这之前就开始了。重叠的区域究竟是什么,可以让两帧共用呢?

当一个方法在执行时,如果一个指令需要参数,解释器会到操作数栈里去装载参数,如果这时的指令是调用一个方法的话(比如invokevirtualinvokestatic),待调用方法的参数应已经顺序存在于操作数栈中,在执行调用指令的时候,这些参数被弹出,成为调用指令的参数,由于操作数栈在帧的最后面,所以这些参数后面再没有本帧的有效数据。这些参数在当前帧的操作数栈中的排列顺序与在新帧的局部变量区中的排列顺序是一样的,而且在新帧中,局部变量区在新帧的最前面,参数列表又在局部变量区的最前面,所以这部分数据是可以重用的,不会丢失有用的信息。

 

程序实现:

压入帧和弹出帧的函数在源文件kvm/vmcommon/src/frame.c中:

void pushFrame(METHOD thisMethod);

void popFrame();

pushFrame()函数的一些关键代码如下:

1      int  thisFrameSize  =  thisMethod -> frameSize;
2      int  thisArgCount  =  thisMethod -> argCount;
3      int  thisLocalCount  =  thisFrameSize  -  thisArgCount;
4     …
5     cell *  prev_sp  =  getSP()  -  thisArgCount;  /*  Very volatile!  */
6     …
7     newFrame  =  (FRAME)(getSP()  +  thisLocalCount  +   1 );
8     …
9      /*  Initialize info needed for popping the stack frame later on  */
10     newFrame -> previousSp  =  prev_sp;
11     newFrame -> previousIp  =  getIP();
12     newFrame -> previousFp  =  getFP();
13     …
14      /*  Initialize the frame to execute the given method  */
15     newFrame -> thisMethod  =  thisMethod;
16     newFrame -> syncObject  =  NIL;  /*  Initialized later if necessary  */
17     …
18      /*  Change virtual machine registers to execute the new method  */
19     setFP(newFrame);
20     setSP((cell * )(newFrame  +   1 -   1 );
21     setIP(thisMethod -> u.java.code);
22     setCP(thisMethod -> ofClass -> constPool);
23     ...

L1-L3分别读出帧的大小、参数列表的大小和本帧实际申请空间的大小(从帧中减去与上一帧复用的部分);

sp是当前栈内的指针,也是操作数的指针,在新的一帧压入之前,sp应指向操作数栈中最后一个参数的位置,所以L5prev_sp所取得的是上一帧中函数参数列表的首地址,也就是新帧开始的位置,以后新方法返回的时候,新帧被弹出,这里应是操作数栈的当前位置,也就是sp的位置,函数的返回值要存放到这里;

L7为新帧申请了空间;

L10-L12为保存调用之前的寄存器状态;

L19-L22为寄存器赋新值。

popFrame()函数比较简单,主要就是调用了下面这个宏来恢复调用前寄存器的值:

#define  POPFRAMEMACRO                                                          
    setSP(getFP()
-> previousSp);     /*  Restore previous stack pointer  */         
    setIP(getFP()
-> previousIp);     /*  Restore previous instruction pointer  */   
    setFP(getFP()
-> previousFp);     /*  Restore previous frame pointer  */         
    setLP(FRAMELOCALS(getFP()));   
/*  Restore previous locals pointer  */        
    setCP(getFP()
-> thisMethod -> ofClass -> constPool);

/*  FRAME (allocated inside execution stacks of threads)  */
struct  frameStruct {
    FRAME    previousFp; 
/*  Stores the previous frame pointer  */
    BYTE
*     previousIp;  /*  Stores the previous program counter  */
    cell
*     previousSp;  /*  Stores the previous stack pointer  */
    METHOD   thisMethod; 
/*  Pointer to the method currently under execution  */
    STACK    stack;      
/*  Stack chunk containing the frame  */
    OBJECT   syncObject; 
/*  Holds monitor object if synchronized method call  */
};
typedef 
struct  frameStruct *             FRAME;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KVM(Kernel-based Virtual Machine)和Docker 是两种不同的虚拟化技术。 KVM 是一种全虚拟化技术,它允许在一台物理服务器上运行多个虚拟机(VMs),每个虚拟机都可以运行自己的操作系统。KVM 基于硬件虚拟化扩展,通过使用宿主机的硬件资源进行虚拟化,提供了接近于原生性能的虚拟机。每个虚拟机都有自己的内核和独立的资源分配,可以运行不同的操作系统,如Linux、Windows等。KVM 提供了强大的隔离性和安全性,适用于需要完全隔离的应用场景。 Docker 是一种容器化技术,它允许将应用程序及其依赖项打包成一个可移植的容器,以实现应用程序的快速部署和可移植性。Docker 使用操作系统级别的虚拟化技术(Linux 容器)来创建和管理容器。容器与宿主机共享操作系统内核,因此可以实现更高的性能和资源利用率。每个容器都可以运行在相同的宿主机上,但它们是相互隔离的。Docker 提供了轻量级、快速启动和高度可扩展的应用程序部署解决方案,适用于微服务架构和容器化应用的场景。 总结来说,KVM 是一种完全虚拟化技术,可以运行多个独立的虚拟机,每个虚拟机有自己的操作系统;而 Docker 是一种容器化技术,可以将应用及其依赖项打包成一个轻量级容器,实现快速部署和可移植性。两者的选择取决于具体的应用需求和场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值