分类: Java
2009-02-11 00:58:35
原文URL:http://weblogs.java.net/blog/mlam/archive/2006/11/the_big_picture.html
亲自的,当你接触一个新系统,第一件事是你想要搞清楚每一个东西是怎么结合在一起的。如果你像我一样是个虚拟思考者,最好的一条路就是画一个图表来说明你觉得重要的东西以及他们之间的联系。嵌入式系统的一个例子,以我的经验,知道内存里有什么在哪儿,系统资源是怎样使用的也同样重要,因此我画了一个数据结构的图表,下面:
根数据结构
CVM的一个设计标准是可重新启动,甚至当你在没有进程的OS上运行它。没有进程需求的可重启性,我们能够释放所有分配的内存。为了让生活更容易(它总是一个很好的实践),我们必须确保所有的数据从单一数据结构书的根上是可获得的。这个根数据结构是CVMglobals,你能在上面地图的左边看到。你会在globals.h和globals.c里找到CVMglobals的定义(也可以在这个文件里找到CVMGlobalState)。看下CVMglobals,你会发现它是一个系统全局数据结构的集合。保证全局在一个地方可以让它更容易恢复已知的初始值。例如:使用memsetting把所有的值设为0(在外面清理了所有的数据后)。
垃圾回收和Java堆栈:
从全局,你可以找到一个内嵌的结构,它把握GC的配置和管理信息(CVMglobals.gc)。从这些,你最终可以得到Java heap。
CVM有可插件的GC体系。插件是编译时的可插件性,不是运行时。它允许在CVM里根据实验来试验GC。当前,唯一一个有品质保证的CVM的GC是世代的(generational)GC(在这儿和这儿可以看到GC的细节执行文件)。
所有的java对象,也就是所有从java.lang.Object扩展的对象,都从java heap上分配。只有一个例外就是ROMized Java objects。它们存在于全局数据。javaheap自己从C heap上分配。所有其它数据结构都从全局数据(i.e. .bss, .data, or their equivalents)或从C heap分配。
JIT和编译后代码
CVMglobals为JIT(CVMglobals.jit)掌握配置和管理记录。通过树,你最后会找到JIT代码缓冲。这个代码缓存当前是固定大小的(通过运行时可配置)它在VM启动时分配。一旦它被分配,它的大小不能被改变。
当java方法被JIT编译,编译器生产的比特(通常作为编译后方法提及)会存在代码缓存中。编译后方法的原数据(meta-data)(由JIT生成)也会存储在代码缓存中。因此,代码缓存的大小会规定,间接的,有多少方法能被编译。
Java对象和类
当类文件装载到内存,内容主要被解析和组织到一个最佳的结构,它从C heap里分配。这个结构叫CVMClassBlock,它掌握所有类的元数据。原数据包括常量池,类属性,域和方法信息,bytecode等等。对每一个CVMClassBlock,有一个从java heap分配的java.lang.Class的实例。一旦一个类已经被装载,它们总是作为一对存在。类块(classblock)有一个参考对一个类,反之亦然。当一个类unload时,它们会一起被释放。
CVM里的每一个java对象有一个2个字的头。第一个字包含了一个指针指向类块。无论如何,这个头对java代码不可见,它只对VM里的C方面可见。注意:自从java.lang.Class扩展自java.lang.Object后,类实例也有2个字的头
查找关键文件在objects.h and classes.h。 看这儿。
Java线程
要执行任何东西,VM必须有线程。每个java线程由CVMExecEnv表现(通常缩写成ee)。在VM,本质上ee是 线程的标示符。所有线程操作请求作为当前执行的线程的ee的参数。看interpreter.h 和 interpreter.c
这儿有一个一对一的映射关系在ee和java.lang.Thread实例之间。一旦线程被初始化,它们2个就作为一对存在。
这儿有一个一对一的映射关系在ee和JNIEnv之间。JNIEnv是一个嵌入的,作为ee的一个域。ee和JNIEnv之间的映射地址的基本需求只是偏移调整。
所有ee被装载在一个链表里。这个链表的头是CVMglobals.threadList。主线程的ee被做为嵌入域在CVMglobals被分配。
系统互斥
VM线程列表的操作需要被同步。在VM里的其他子系统和资源都一样。同步一般都由CVMSysMutex(看sync.h和sync.c)。在VM启动时有几个系统互斥量(sysMutexes)被分配。这些互斥量只对VM的C代码可见。只能被C代码使用。
每个互斥量都有明确的目的(CVMglobals.threadLock用来同步线程),它是有序的。为了防止死锁,系统互斥量只能在增加序号时被锁住。当CVM使用断言建立时,升序可以被断言。
Java执行堆
一个执行的线程必须有一个执行堆。在CVM里,每一个Java线程有2个物理堆:一个本地堆,一个java堆。本地堆由系统分配,用于C代码执行。它掌握本地代码的活动记录(堆帧 stack frames),VM代码包括解释器循环功能。它掌握所有JIT编译后代码的活动帧。
Java堆(也叫解释器堆)用来保存所有java方法的活动记录。每个java方法一执行,一个帧会推入堆中。堆和帧数据结构定义在stacks.h和stacks.c中。
如果你在执行java方法时dump一个本地堆的跟踪,你会看到C代码的堆帧(stack frames)和解释器循环。如果你dump一个java堆,你只能看到java代码的堆帧当它们被调用时。如果你有一个本地方法在执行链(invocation chain),你可以看到本地堆和java堆的堆帧。这是因为本地方法是C功能和java方法的和。
GC根和根堆
在GC阶段,CVM被称为一个精确的VM。这意味着在GC时间,我们能够确切的知道所有的对象指针在系统的哪个地方。这和需要你猜测某个内存片包含对象指针和一些类似对象指针的随即数据的老式GC系统有鲜明的对比。
VM里所有能获得的对象能够通过跟踪被叫做垃圾回收根树的对象参考树来找到。这个树从一个根参考开始。这些根参考本质上是全局的。用来存储被称为根堆(root stacks)的数据结构。有一个例子是CVMglobals.globalRoots。准确的说,这些数据结构不需要成为堆。它们被作为链表来使用。无论如何,我们的java堆数据结构有道具来实现GC根堆的需要,不要请求我们写附加的代码(为了代码效率),因此我们只使用堆。
如果一个对象不能通过跟踪根树来找到,那么对象是不可到的,它可以被GC再生。
注意在穿越树中,任何的点,一个节点都可能是新的子树的根。因此,term root或者GC根有时被用来指向一个对象指针/参考,它总能通过根查找来找到。GC根能在根堆里找到,在线成执行堆,在对象和类域里。
终了
给了你们上面的主意足够让你们了解CVM里最主要的数据结构。注意:我告诉你们的大部分事给了你们一个很好的概念模型。实践上,这儿有一系列的原因来导致异常。某些时候,这些异常会打破规则。另外一些时候,他们看起来象是扩展了规则。为了保持事情简单,我忽略了它们。当我在谈到每个子系统或特殊的数据结构的时候我会详细讨论它们。
在上面,我忽略了一些有趣的细节,比如为什么从C heap 分配数据结构相对于java heap。几天或几周后,我会放大CVM的子系统或数据结构,详细的讨论它们。这包括机械的细节作为设计体系。
祝有美好的一天!!