CVM栈和代码的执行

原文链接:http://weblogs.java.net/blog/2006/11/30/cvm-stacks-and-code-execution

 

CVM栈和代码的执行


欢迎来继续讨论phoneME Advanced VM(CVM)的内部结构。如果你错误过了我们之前的讨论,就先从一个高度来回顾CVM架构图中主要的VM的数据结构。现在,将会深入Java代码的执行,以揭示运行时Java方法构成的栈的结构。这个栈记录了Java线程运行时的方法的信息,而不是包含了Java虚拟机的运行环境的栈,也不是API层所谓的栈。这一节会揭示CVM中Java代码执行控制流的细节(也就是某个时刻,哪个方法正在占用CPU时间)。

 

所有的源代码可以参考phoneME Advanced工程中的src/share/javavm/includesrc/share/javavm/runtime两个目录。Include目录中包含头文件,runtime目录中包含C源文件。

 

执行引擎

在CVM中,除了一个解释器之外,还有一个动态适配编译器(即为人们熟悉的JIT)。理论上,解释器只是一个很大的switch语句,其中每个case语句对应执行一个字节码(见executejava_standard.c中的CVMgcUnsafeExecuteJavaMethod())。解释器不断循环执行这个switch语句,直到字节码执行结束。对执行频率很高的方法(常被称为hot),JIT就会将这些方法编译为本地机器码。执行编译后的方法的代码代替了解释字节码。

 

测量一个方法执行频度的办法有很多。CLDC VM(phoneME Feature)使用一个基于取样机制的计时器。在撰写本文之前,CVM在解释时保存方法的调用计数。当达到一个阈值时,就将该方法编译为本地机器码。那么如何从解释字节码转为执行编译后的方法?为理解这一点(以及所有Java代码执行与其它代码执行的细微差别),必须看一下Java代码执行时,运行时栈都做了些什么。

 

运行时栈

如前所述,CVM中的每个Java线程有两个栈:一个本地栈和一个Java栈。Java栈一般也被称为解释器栈。CVM中的每个线程有一个指向CVMExecEnv记录的指针,一般把它称为ee。在得到ee之后,就可以得到它的Java栈,通过以下语句:

CVMStack *currentStack = &ee->interpreterStack;

 

Java栈

Java栈是一个CVMStack类型的结构(见stacks.h和stacks.c)。每个栈有一组栈段构成的链表。当在CVMinitStack()初始化栈的时候,就用malloc分配一个栈段。随着栈需要的内存越来越多,就需要分配更多的栈段。因此,Java栈以栈段的形式增长。理论上,栈段也可以收缩,但现在的代码没有实现这一点。

 

方法的执行过程记录在栈段中。最基本的帧是CVMFrame(见stacks.h)。还有CVMFreeListFrame(见stacks.h)、CVMJavaFrameCVMTransitionFrameCVMCompiledFrame (见interpreter.h)。这些帧结构都是CVMFrame的派生结构。注意:这里之所以不将它们称为子类是因为CVM是用C写的,但采用了面向对象的概念进行设计,很多数据结构都可以理解为多态,因为这样做很有意义。

 

这些CVMFrames组成一组帧的链表并分跨在栈段中。链表的头(也就是第一帧,栈底)也被认为是第一段的开始。CVMStack中的currentFrame指针指向链表中的最后一帧(也就是栈顶)。

 

CVMJavaFrame
CVMJavaFrame是放置被解释成字节码后的帧的位置。在调用一个方法之前,VM会向栈中推入一帧CVMJavaFrame。初始化这一帧时,也会初始化一些信息,如CVMMethodBlock * 信息。方法的元数据保存在被称为CVMMethodBlock的数据结构中(通常叫做mb或MB)。MB的地址也被作为这个方法的全局唯一标识。这帧中保存了这些信息。帧中还包含一个程序计数器值(PC)。PC指向下一条将要执行的字节码(就像方法调用中的调用者PC一样)。当前PC并不总是填充到当前帧之中,而保存在解释器循环的本地状态中。

 

帧的结构如下所示:

                              |-----------------|

      start of frame --->     | locals ...      |

                              |-----------------|

                              |    frame info   |

                              | (CVMJavaFrame)  |

                              |-----------------|

      top of stack   --->     |  operand stack  |

                              |        ...      |

                              |-----------------|

local段保存了Java的局部变量(正如VM规范中声明的那样),操作数栈段用于保存作为VM字节码计算时推入或弹出的操作数,或为下一个被调用方法的参数,或为方法调用结束的返回值。VM规范中声明给定字节码的方法在调用之前就应确定局部变量的个数和操作数占用空间的大小。因此,在向栈推入一帧之前就应确保栈段还有足够的空间。如果没有足够的空间,就应分配一个新的栈段,这一帧会被推入新的栈段中。

 

因为输出参数(用于下一个将被调用的方法)保存在操作数栈中,这部分操作数栈作为下一帧的local段,如下所示:

                          |-----------------|

      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规范一致了。因为规范声明输入参数从帧的local段的0地址处开始。

 

注意:在CVM中,local段和操作数栈段的基本单位是word。在32位系统上,这表示32位的内存。栈指针以word为单位增加。这些word会包含Java原始类型的数据(64位值将占两个单元)或对象指针。

 

CVMFreeListFrame

JNI方法将帧作为空闲列表来使用。帧的结构如下所示:

                          |--------------------|

      start of frame ---> |   frame info       |

                          | (CVMFreeListFrame) |

                          |--------------------|

                          | operand stack      |

       top of stack ----> |     ...            |

                          |--------------------|

JNI方法帧没有输入参数或任何局部变量,输入参数被保存在本地方法的本地栈帧上。这些参数从调用者帧操作数栈上的输出参数复制而来。这是由交合代码中invokeNative汇编程序段列集操作完成的(如invokeNative_arm.S中的CVMjniInvokeNative)。

 

JNI方法与Java方法的另一个区别是它的操作数栈段只被用于保存对象指针。其它操作数保存在CPU寄存器或本地栈中(这依赖于编译这个本地方法的C编译器)。在JNI中,当使用NewLocalRef分配本地引用,空闲列表帧分配了一段内存单元保存对象指针。当使用DeleteLocalRef释放分配的栈段时,这些已分配单元会被链入freeList(就是freeList帧的名字)的链表中。链表的头部在CVMFreeListFrame记录中。JNI方法的MB指针的一个副本也被保存在那里。当分配一个本地引用,首先检查freeList是否有可供使用的引用。如果有一个可供使用,这个引用就从链表中删除并返回。如果链表中没有可供使用,就增加栈顶指针的值并从操作数栈顶部开始分配。

 

与Java字节码方法不同的是,JNI方法并不知道最多要分配多少个操作数。幸运的是,并不必须知道。与Java帧不同,freeList帧可以跨越栈段。如果当前栈段空间不够,就加入另一栈段,并从新的栈段上分配。

 

正如之前的文章所述,freeList帧的另一个用途是为了实现GC根栈。GC根栈的实现是使用一个只包含一个freeList帧的CVMStack。GC根栈只是对象引用的一个列表,它扮演着GC时对象引用的根的角色,这个根中的单元可以被分配和释放(比如,当调用JNI的NewGlobalRoot()DeleteGlobalRoot())。freeList帧有效地实现了这个功能。

 

CVMTransitionFrame

转换帧是为了省去大量用于胶合的代码而使解释器直接调用方法的技巧。其工作原理就是使用指向被调用方法的常量池条目来模拟字节码方法。常量池条目并不是一个常量,在解释器循环中,是会改变的变量。解释器常量池条目设为指向目标方法的MB。然后,它调用4条称为转换方法的伪方法(见executejava_standard.c文件中的CVMinvokeStaticTransitionCode,CVMinvokeVirtualTransitionCodeCVMinvokeNonVirtualTransitionCode,和CVMinvokeInterfaceTransitionCode)。使用哪个转换方法依赖于方法调用的类型。

 

这个机制的用途之一是调用静态初始化方法(<clinit>)。这个机制还用于调用Java代码的第一个方法时。

 

CVMCompiledFrame

最后介绍一下预编译帧。帧的结构如下所示:

                          |--------------------|

      start of frame ---> |   locals ...       |

                          |--------------------|

                          |   frame info       |

                          | (CVMCompiledFrame) |

                          |--------------------|

                          |  spill/temp area   |

                          |--------------------|

                          |    ...             |

       top of stack ----> |  operand stack     |

                          |    ...             |

                          |--------------------|

就像Java帧一样,CVMCompiledFrame包含一个MB指针和一个PC。在这种情况下,PC指向下一条要执行的指令(包括返回时的调用者PC)。VM将要调用一个方法时,首先检查这个方法是否被预编译,或是否是本地方法。如果是本地方法,在使用用于胶合的invokeNative汇编程序调用方法之前会推入一个freeList帧。如果是非预编译的字节码方法,就推入一个Java帧,继续解释器循环的执行。如果方法是预编译的,就会推入一个预编译的帧,然后VM跳入预编译的方法的入口处继续执行。

 

栈上替换(OSR)

如果一个被解释的方法在循环中要运行很长时间,同时又要预编译它。如何在一个被解释了的方法运行到一半时转而继续运行它的预编译版本?可以使用一个称为栈上替换(OSR)的特性。OSR允许将栈中的Java帧替换为一个等价的预编译帧。

 

预编译帧的结构很像一个Java帧。这里可见的唯一区别是多了一个填充段/临时段。这个段有固定的大小,而且在方法被编译以后这个段的大小就确定了(也就是说这一帧被推入之前)。预编译帧可以拥有比等价的Java帧更多的局部变量,因为预编译帧的方法是内联的。操作数栈的大小也不同。在设计上,CVM JIT为预编译方法保存和等价的字节码方法同样多的局部变量。为方法内联而增加的局部变量拥有较大的下标。这意味着将Java帧的局部变量映射到预编译帧的局部变量是很容易的。而预编译帧的额外信息可方便地由Java帧计算得到。

 

剩下的是关于填充段和操作数栈的内容。基于对Java语言编译后的字节码的观察,在循环开始时操作数栈是空的。以现今的javac编译器来说,99%的情况是这样。运行频率最高的循环是适合使用OSR的。开始一个循环不需要对操作数栈做任何映射,可以利用这一点运用OSR。

 

对于填充段而言,开始循环时,CVM JIT也不会产生任何填充内容。因此,此时唯一需要做的是为新帧保留一些空间。正因如此,可以将被部分解释的运行频率高的循环替换为预编译的等价的代码。

 

注意:CVM仅支持将Java帧替换为等价的预编译帧的OSR,反之则不可。执行相反方向的OSR的代价昂贵得多,不适用于JavaME系统。这也是Java社区应在未来关心的问题之一。关于反向的OSR有些有趣的高级特性,会留在未来使用它时再讨论。

 

关于下一讲

现在已经介绍了CVM的栈结构。下一次将要简要地介绍本地栈帧(任何嵌入式程序员应已非常熟悉),这将会更有趣,还会介绍两者的相互影响。那么,让我们期待这个讨论的第二部分。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值