您的位置:首页 > 其它

The BIG Picture : a Map of CVM

2009-10-13 18:00 218 查看
原文连接:http://weblogs.java.net/blog/mlam/archive/2006/11/the_big_picture.html

纵观全局:CVM架构图

就我个人而言,当考察一个新的系统的时候,第一件要做的事情就是弄清楚各个模块是如何组织在一起的。如果你像我一样是一个明察秋毫的思考者,最好的方法之一就是绘制一张所有你认为重要的模块的关系图。对于嵌入式系统,以我的经验,很重要的一点是清楚程序在内存中是怎样的,以及了解系统资源是如何被使用的。因此,我更倾向于给出数据结构的架构图。

这是CVM的架构图……

根数据结构
CVM的设计准则之一是即使在不基于进程的操作系统之上虚拟机也是可以重新启动的。做到不使用进程也可重启要求我们释放所有已分配的内存。为更方便地做到这一点(同时也是很好的做法),就要保证所有的数据可在一个从根开始的树形数据结构上被访问到。根的数据结构就在图的左侧。CVMglobals在globals.h中(同时参考CVMGlobalState)和global.c中的定义。CVMglobals中聚集了系统全局数据结构。将全局数据放在一起,便于将全局恢复到一个初始状态,要做到这一点只需将这段内存全设置为0(当然,要在清除了所有子树之后)。

垃圾收集与Java
全局变量CVMglobals中包含了GC配置与管理信息(CVMglobals.gc),从这里就可以得到Java堆。CVM有一个可插拔的GC架构,这里可插拔是指编译时可插拔而非运行时可插拔。这允许在CVM中试验其它GC。目前,CVM产品级GC就是分代GC。所有Java对象,只要是继承自java.lang.Object,都在Java堆上分配。唯一的例外就是ROMized Java对象。它们保存在全局数据中。Java堆本身从C堆上分配。所有其它数据结构或从全局数据(即.bss,.data或它们的等价物)中分配,或从C堆上分配。

JIT和编译后的代码
CVMglobals也保存有JIT的配置和管理记录(CVMglobals.jit)。遍历这棵树,就可以看到JIT代码缓冲区。代码缓冲区的大小是固定的(虽然是运行时可配置的),是在VM启动时分配的。一旦分配,大小便不能更改。当java方法由JIT编译之后,编译器生成的代码(也就是编译后的方法)会被保存在代码缓冲区中。编译后方法的元数据(由JIT生成)也保存在代码缓冲区中。因此,代码缓冲区的大小间接地决定了可以有多少个方法被编译。

Java对象和类
当一个class文件被载入内存的时候,它的内容被解析和组织为一个优化了的结构,这个结构是从C堆上被分配的。这个结构被称为CVMClassBlock,它保存有类的所有元数据。元数据包括常量池、类属性成员、方法信息、字节码等等。对于每一个CVMClassBlock,都对应一个java.lang.Class的实例,它从Java堆上分配。一旦一个类被正确地加载,它们(指class和CVMClassBlock)就会成对存在。ClassBlock有对class的引用,反过来也一样。当类被卸载的时候,它们会同时被高效地释放。

CVM中的每个Java对象有两个字大小的头部。第一个字一般包含指向class模块的指针。然而,这个头部对Java代码是不可见的。它只对VM的C代码可见。注意:由于java.lang.Class继承自java.lang.Object,Class的实例也包含这两个字构成的头部。

细节请见objects.h和classes.h两个文件。

Java线程
为了执行任何任务,VM必须拥有线程。每个Java线程由一个CVMExecEnv表示(通常也被称为一个ee)。在VM中,ee是线程必需的令牌标识。所有线程的操作需要当前执行的线程作为参数。请见interpreter.h和interpreter.c两个文件。

ee和java.lang.Thread之间存在1对1的映射。一旦线程被适当地初始化,两者就会成对地存在。

ee与JNIEnv之间也存在1对1的映射。JNIEnv作为ee中的一个成员。在ee和JNIEnv之间的映射本质上只需要改变一个偏移量即可。

所有ee都被链接成一个链表。链表头就是CVMglobals.threadList。主线程的ee被分配为CVMglobals中的一个成员。其它成员由malloc分配。

系统互斥变量
有时要同步对VM线程列表的操作。VM中的其它子系统和资源也是如此。同步一般是使用一个CVMSysMutex对象(请见sync.h和sync.c)。在VM启动时会分配若干sysMutex。这些互斥变量对Java代码并不可见,仅对VM的C代码可见。它只在VM的C代码中用到。Java代码并不用到。每个sysMutex有特定的用途(比如CVMglobals.threadLock被用于同步线程操作),且有优先级的区别。当CVM编译时开启断言功能,那么优先级的次序就可由断言来保证。

Java执行栈
任何正在执行的线程都有一个执行栈。在CVM中,每个Java线程有2个物理栈,一个本地栈,一个Java栈。本地栈由OS分配,在C代码执行中用到。它保存本地代码的运行记录(也就是栈帧)和解释器循环函数的VM代码。它也保存JIT编译后代码的活动帧(但不是那么直接地保存)。

Java栈(也称为解释器栈),用于保存Java方法的活动记录。每个执行的Java方法,会在栈中压入一帧。栈和帧的数据结构在stacks.h和stacks.c两个文件中定义。

如果在执行几个Java方法时倾印出本地栈的信息,就可以看到C代码的栈帧和解释器循环。如果仅倾印出Java栈,那么只会看到已经被调用的Java方法的栈帧。如果在调用链中存在一个本地方法,那么在本地栈和Java栈中它都会出现。这是因为本地方法既是一个C函数又是一个Java方法。

GC根和根栈
在GC术语中,CVM被称为一个完整的VM。这意味着当GC的时候,可以完全确定系统中的所有对象指针在哪里。与之相对的保守GC系统,它需要猜测某段内存是否包含对象指针,还是看似对象指针实则为随机数据。

VM中的所有可触及对象(因此也是活动的)可以从GC树的树根处开始的遍历访问到。这棵树由一个对根的引用开始。这些对根的引用必须是全局的,一般保存在被称为根栈的数据结构中。典型的例子就是CVMglobals.globalRoots。严格地说,这些数据结构不一定必须是栈。实际上它们被作为列表使用。然而,Java栈数据结构的属性完全符合GC根栈的需求,不要编写额外的代码。所以就使用栈数据结构。

如果对象不能从根树开始遍历到,那么该对象就是不可触及的,就应该被GC回收。

遍历一颗树的时候,遍历时的任何一个节点都可能是个新的子树的根。因此,术语根或GC根有时被用于指从根开始遍历到的指针/引用。可以在根栈、线程执行和对象或类的成员中访问GC根。

尾声
现在应已足以使你了解了CVM中主要数据结构的整体概念。上述内容意在给你一个整体架构的概念模型。实际问题中,在某些情况下,在各种条件的影响下也可能有例外。有时,例外情况会与之前所述的内容矛盾。还有些时候,它们可能是之前所述的内容的延伸。为了使描述简单化,可以先不考虑这些例外情况。之后会在讨论特定子系统或数据结构中专门讨论这些例外情况。

就上述内容,已经省去了许多具体细节。比如是从C堆上还是从Java堆上分配某个数据结构。这会留在以后讨论。

在之后的几天(或几周中),我会深入CVM每个子系统或数据结构详细地讨论。这会包括每个细节以及有关的设计哲学即为什么要这样设计。请随时提问或发表看法。我会尽量满足网友的请求。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: