接下来的两篇将介绍在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;
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐