原文URL:http://weblogs.java.net/blog/mlam/archive/2006/11/cvm_stacks_and_1.html
欢迎继续讨论phonME Advanced VM(CVM)的内部联系。如果你错过了开始的讨论,你可以在这儿看到我通过CVM地图来对VM主要的堆数据结构的一个介绍。今天,我们会深入到java方法的执行和它们在运行时堆中的表现。我说的这个堆是线程里面用来记录活动方法的堆。不是包含API或API层的堆。这次的讨论会给你洞察CVM里面java代码执行的控制流(比如任何时候谁拥有CPU)。所有的代码都可以在phonME Advanced的工程文件夹下的src/share/javavm/include和src/share/javavm/runtime找到。
执行引擎
在CVM中,有一个解释器,一个动态编译器(JIT)。从概率上来说,解释器就是一个大的switch声明。每一个case对应一个bytecode用来执行(在executejava_standard.c文件中看CVMgcUnsafeExecuteJavaMethod() 函数)。解释器围绕switch声明循环,直到没有bytecode需要执行。对那些频繁执行的方法(我们一般叫它们热点),JIT会编译这些方法到本地机器代码。编译后的方法会在做bytecode解释的一个地方被执行。
这儿有许多的方式来测试方法的热烈。CLDC VM使用一个基于取样机制的计时器。象写的这样,CVM使用采样祈祷(invocation)计数在整个解释期间。上面到达了一些开始的热点,方法得到编译。这个问题现在变成了怎么样从解释器bytecode到执行编译后的方法。为了理解这个(所有其他java代码执行的要点),我们需要看看在运行时堆里发生了什么,当java代码执行的时候。
运行时堆
象前面说的,CVM里的每一个java线程有2个堆:一个本地堆和一个java堆。Java堆一般作为解释器堆。CVM里的每一个线程是一个标示作为CVMExecEnv记录的一个指针。我们通常把这叫做ee,通过ee,我们总能象下面这样找到java堆:
CVMStack *currentStack = &ee->interpreterStack;
Java堆
Java堆是CVMStack的类型(可用看stacks.h和stacks.h)。这个堆被组织成一个堆块链表的样子。当堆在CVMinitStack()里被初始化时,它会分配一个堆块。如果一个堆里需要更多的内存,那么附加的块会被分配。因此java堆是可增长的。理论上,堆也能够被缩小,但现在的代码不支持这样做。
方法活动记录存储在结构(frame)中。基本的类结构是CVMFrame(看stacks.h)。这儿也有CVMFreeListFrame(看stacks.h),CVMJavaFrame,CVMTransitionFrame和CVMCompiledFrame(看interpreter.h)。所有的这些结构都CVMFrame的多态。注意:尽管CVM是用C写的,它使用了一些对象导向的范例在它的设计中。一些数据结构是多态在制造它的感觉的地方。
CVMFrame来自frames链表并能跨过堆块。这些链表的头(bottom-most frame in the stack)总是已知的因为它总是堆的第一块。链表的最后一个结构(top most frame in stack)是CVMStack里面currentFrame指针
CVMJavaFrame
CVMJavaFrame是一个用在bytecode解释器方法里的结构。在调用方法前,VM会把CVMJavaFrame推入堆。结构会根据信息被初始化,信息通过调用CVMMethodBlock *来得到。方法元数据存储在被叫做CVMMethodBlock的数据结构中(通常叫mb或MB)。MB的地址被用来作为方法的唯一标示。因此,这就是什么存储在结构中了。结构也包含程序计数器值(program counter = PC)。在这个例子中,PC是一个指向下一个要执行的bytecode的指针(作为方法调用的返回)。当前PC不总是最新的结构。代替它的是保持在解释器循环的本地状态。
Frame的结构看起来象下面:
|-----------------|
start of frame ---> | locals ... |
|-----------------|
| frame info |
| (CVMJavaFrame) |
|-----------------|
top of stack ----> | operand stack |
| ... |
|-----------------|
Locals区域掌握java locals(在VM spec中定义),操作数堆区域是VM推入推出操作数的地方,这些操作数用来推算opcode,或者是被调用方法的外部(outgoing)参数,或者是刚刚被调用方法的返回值。
VM spec说locals的数量和最大操作数堆容量在任何给定的bytecode方法的时间前是已知的。因此,我们知道如果堆块有足够的房间剩余,在我们把frame推入前。如果没有,那么一个新的堆块将被分配,frame将被推入下一个块实例。
自从外部(outgoing)参数(对于下一个被调用的方法)存储在操作数堆,操作数堆的一部分成为locals区域的开始,对于下一个frame来说,象下面:
|-----------------|
start of frame ---> | locals ... |
|-----------------|
| Method 1 |
| frame info |
|-----------------|
| operand stack |
| |-----------------|
start of next frame ---> | outgoing args = incoming locals |
| | |
|-----------------| |
|-----------------|
| Method 2 |
| frame info |
|-----------------|
top of stack ---------------------> | operand stack |
| |
|-----------------|
这是VM spec说的是一致的,传入的参数开始于0索引的局部区域frame。
注意:在CVM中,局部和操作数堆是字空间槽(slot)。在32-bit系统中,这意味着32-bit的内存。堆指针的增长是字增长。这些字能够包含java私有类型(64bit值会占2个槽),或者对象指针。
CVMFreeListFrame
一个使用freelist frame的是JNI方法的frame。Frame的结构如下:
|--------------------|
start of frame ---> | frame info |
| (CVMFreeListFrame) |
|--------------------|
| operand stack |
top of stack ----> | ... |
|--------------------|
一个不同是这儿没有引入的lacals或者任何种类的locals。JNI方法,传入的参数存储在本地方法的堆栈结构中。这些参数是调用结构(caller frame)操作数堆栈中外部参数的一个拷贝。这个拷贝是汇编调用本地粘和的一部份(在这儿查看invokeNative_arm.S中的CVMjniInvokeNative)。
另一个不同是操作数堆栈区域只是用来存储对象指针。其他操作数存储在CPU寄存器或本地堆栈(依靠C编译器编译的本地方法)。在JNI中,当你使用NewLocalRef分配局部引用,存储对象指针的堆栈分配一个freelist结构。当你使用DeleteLocalRef释放引用,堆栈slot从一个叫freelist的链表中得到chained。列表的头是CVMFreeListFrame中的记录。JNI方法的MB指针的一个拷贝也存储在这儿。当你分配一个局部引用,我们首先检查freelist的可用引用,如果有一个可用,引用被从列表里移动并返回。如果没有可用,我们碰(bump)顶部的堆栈指针,从操作数堆栈的顶部分配。
注意,不象Java bytecode方法,我们不知道操作数能分配的最大数。幸运的我也不需要知道。不象Java frame,freelist结构能够横跨堆栈块。如果我们在当前块的外部运行,我们简单的添加从其他新的块分配的块到堆栈。
Freelist其他用法是执行GC根堆栈。GC根堆栈执行使用一个CVMStack和在它里面的一个freelist。因此GC根堆栈是真正假象的对象引用列表,几个GC根能够分配和释放(当你调用JNI的NewGlobalRoot()和DeleteGlobalRoot()方法),freelist frame是很好的执行。
CVMTransitionFrame
传输结构机制(transition frame mechanism)是一个聪明的诀窍为了得到解释器调用方法,为我们不需要写一大堆代码。它的工作是连同一个特别的指向一个需要调用的目标方法的常量池入口模拟一个byte code方法。常量池入口不少真正的常量,但它可在解释器循环中使用。解释器设置常量池入口指向目标方法的MB。接着,它要求调用4个叫transition假方法(看executejava_standard.c中的这些方法CVMinvokeStaticTransitionCode, CVMinvokeVirtualTransitionCode,CVMinvokeNonVirtualTransitionCode, CVMinvokeInterfaceTransitionCode)中的一个。这些传输方法的选择依靠我们想要调用的类型。
这个机制用来调用静态初始化方法,另一个为了一步一步的进入java代码里的第一个方法。
CVMCompiledFrame
最后,我们解释编译结构,如下:
|--------------------|
start of frame ---> | locals ... |
|--------------------|
| frame info |
| (CVMCompiledFrame) |
|--------------------|
| spill/temp area |
|--------------------|
| ... |
top of stack ----> | operand stack |
| ... |
|--------------------|
象Java结构,CVMCompiledFrame包含了一个MB指针和一个PC。在这里PC是一个指向编译后代码(指示下一个执行)的指针(返回PC类型)。当VM要调用方法时,它首先检查方法是否编译了,是否是本地代码。对本地代码来说,freelist frame被调用本地粘合汇编代码推入在方法被调用前。“未编译”或bytecode方法有一个java结构推入,在解释器循环中连续执行。如果方法已编译,一个编译后结构会被推入,VM会直接跳到指向编译后方法的指针处继续执行。
On-Stack交换
如果方法已经在循环中被解释很长的时间,我们决定编译它。。。当我们已经解释了一半的方法,那我们怎么样继续执行编译后的方法呢?我们需要的一个特性叫做On-Stack Replacement(OSR)。OSR允许我们替换堆里面的java frame用一个相同的编译后frame。
注意,编译后frame的形态和java frame非常相似。唯一的不同是附加的spill/temp区域。这个区域在方法编译后是已知固定大小的(在frame被push前)。这儿的确有其他的不同。一个编译后frame能够有更多的局部比java frame副本当方法内联期间。同样,操作数堆区域的大小也不同。在设计上,CVM JIT保持同样的locals映射对编译后方法作为相应的bytecode。那些从内联方法加入的locals被作为更高的索引被添加。这意味着它能更容易的从java frame到编译后frame。作为编译后frame新特性信息,我们能计算从java frame 信息中不需要的成就
离开spill区域和操作数堆,一个我们制造的观察是因为自然生成的bytecode从编辑所有的java语言。操作数堆会趋向为空,当循环开始的时候。这在今天的javac编译器情况下有99%是真的(这是一个疯狂的猜测但极可能是对的)。Hot循环在我们想要OSR发生的地方。这意味着在操作数堆上没有什么被映射当我们开始循环的时候,我们能够取得优势来做OSR。
作为spill区域,CVM JIT不能产生溢出内容在循环的开始。因此,唯一需要做的一件事是为它在新的frame中储备一些空间。因为这些,我们能替换hot loops,用同样编译后的替换部分解释器。
注意:CVM只支持java frame的OSR连同他们对等的编译后frame,而不是其他方向。其它方向中的OSR是值得注意的更多的困难和更多的招数导致更多的开销而不适合JavaME系统。这会是一个区域,那些团体能够选择调查在未来,如果它希望。这儿有一些有趣的高级的事能够和OSR相反的来做,如果我们从来没有接触它,我为在其他时候离开。
下一个是什么?
我们已经介绍了java stack结构和CVM。明天,我会简要的谈论关于本地stack frame(那些嵌入式程序员应该很熟悉它)和更多的有趣的。我会谈论他们之间的相互影响,因此,请看这个讨论的第2部分。
祝有美好的一天!!