随笔 - 42  文章 - 71  trackbacks - 0
<2008年4月>
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

常用链接

留言簿

随笔档案

文章分类

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜

译者前言

本文介绍了IBM JDK 1.2.2到1.4.1 SR1版本垃圾收集原理,虚拟机内部内存分配及管理的机制。根据IBM的说明,本文档也适合JDK 1.4.2。总体感觉翻译这篇文档比Sun HotSpot虚拟机的内存管理机制的那一篇要更加吃力一些。文档中介绍了很多的细节,也有较多的陌生的词。在文档的最后附有中英文词汇对照表。如果有错误之处,希望大家踊跃指出。如果你的英语水平可以的话,建议去读原文。

了解虚拟机内部内存管理以及垃圾收集的机制,可能有助于你调优虚拟机。另外,翻译这篇文章主要的原因是IBM JVM 和HotSpot JVM的有很大的区别,基本上是两套差异较大的思路出来的产品。HotSpot是分代的,IBM JVM是不分的。但是根据IBM的文档,在1.5以及后续的版本中,提供了分代收集的策略。JRockit可以同时支持分代和不分代的两种策略。看来大家都在互相取长补短啊。

原文出自

http://www-128.ibm.com/developerworks/java/jdk/diagnosis/142.html

Garbage Collection and Storage Allocation techniques

转载请注明出处(http://www.blogjava.net/yoda/),谢谢!

1 简介

本文档描述从1.2.2到1.4.1 SR1的存储组件(ST)功能。(译者注:根据IBM的说明,本文档也适合JDK 1.4.2)

存储组件(Storage Component)用来分配堆中的存储空间,这些存储空间定义了对象、数组以及类。每个对象在存储空间中占有一部分存储,如果从虚拟机活动状态中存在到这个对象的引用(指针),这个对象就是可到达的;当虚拟机活动状态不再存在到这个对象的引用,这个对象就被视为垃圾,他占用的存储空间可以被重新使用。当申请重用发生时,垃圾收集器必须进行一些清理工作,以确保该对象所关联的监视器能够被释放回对应的监视器池中。存储组件并不是等同对待所有的对象,比如类对象以及线程对象是在堆的一个特殊空间(Pinned Cluster,固化簇)内进行分配的;在追踪整个堆的时候,引用对象和其派生对象也是被特殊处理的。在4.4章"引用对象"中对于这种特殊情况有详细的阐述。

1.1 对象分配

对象分配是由对于某个分配接口的调用来驱动的,例如stCacheAlloc, stAllocObject, stAllocArray, stAllocClass。调用这些接口会在堆上分配一块空间,但是调用时的参数有所不同。stCacheAlloc函数是为小对象的高效分配所设计的,每个线程会预先从堆中申请一块独享的空间,叫做线程本地堆(Thread Local Heap),简称TLH。小对象在这个空间内直接进行分配。一个新的对象从线程本地堆的底部开始分配,由于无需获取堆锁,所以这种分配动作是非常高效的。如果对象较小(当前上限是512字节),那么即使使用stAllocObject和stAllocArray接口,对象也会分配在线程本地堆。

1.2 可到达对象

线程执行栈、类内部的静态对象以及本地和全局的JNI引用共同构成了虚拟机的活动状态。虚拟机内部调用的函数会导致在C执行栈上生成一个帧,这些信息用来寻找根对象,从根对象出发来寻找被引用的对象。这个过程会一直重复直到找到全部可到达对象。

1.3 垃圾收集

当堆内存空间不足,导致虚拟机内存分配失败时,就会发生垃圾收集。垃圾收集的第一步工作就是找到堆中全部的垃圾,这个工作可以由任何线程的内存分配失败激活,也可以由显式调用System.gc()函数激活。首先,要获取垃圾收集所需的全部锁,这可以保证对于当时拥有临界锁的线程不会被挂起。通过执行管理接口(execution manager, XM)来挂起其他线程,以确保对于调用线程,其他线程的挂起状态是可进入的,这个状态包括在挂起时刻的从顶到底的执行栈以及寄存器状态,以用来追踪对象引用。在此之后,垃圾收集才可以开始工作,包含3个阶段:

· 标识

· 清理

· 压缩(可选的)

1.3.1 标识阶段

在标识阶段,所有的被虚拟机活动状态引用,或者静态的,或者固化字符串以及被JNI引用的对象都被标识。这个动作创建了JVM引用的根对象,这些根对象可能会依次引用其他对象,因此,标识阶段的第二部分工作就是从根对象出发,扫描其他被引用对象。这两步工作产生一个活动对象集合。

分配集合(allocbits)中的每个比特位标识堆中的一个8字节段,一旦分配了一个对象,分配集合中的对应比特位会被标识。垃圾收集器开始追踪栈时,首先比较指向堆底和堆顶的指针,确保指针指向的是8字节边界的对象,然后对分配集合中对应的比特位进行标识,表示该指针指向一个活动对象。然后在标识集合(markbits)中对对应的比特位进行标识,表明该对象处于被引用状态。

最后,垃圾收集器扫描对象的域字段查找被这个对象引用的其他对象,这个扫描过程是准确完成的,因为方法指针存储在第一个字单元,垃圾收集器能够知道对象的类型。在对象链接时(对象第一个实例创建之前),类装载器会创建一个偏移集合,这个偏移集合中记录了对象中引用其他对象的字段偏移位置,垃圾收集器通过访问这个偏移集合找到对应的域及其引用的其他对象。

1.3.2 清理阶段

标识阶段之后,在标识集合中包含了堆中所有可到达对象的标志。标识集合必须是分配集合的一个子集,清理阶段的工作就是找到这两个集合之间的差集,也就是在那些已经分配但是不再被引用的对象。

最起初,清理阶段就是从堆底开始扫描,依次访问堆中的每个对象。对象的长度存储在一个字单元中,对于每个对象,垃圾收集器检测对应的分配标识位和标识标识位以定位垃圾。

现在,使用bitsweep技术无需扫描整个堆,这样就避免了页交换的额外消耗。bitsweep技术直接在标识集合中寻找长的连续的0(未被标识的对象),这段长的连续的0可能表示一段空闲空间。找到这样的序列之后,垃圾收集器检测序列开始前的对象的长度,以检测可以被释放的空闲空间的大小。

1.3.3 压缩阶段

垃圾收集器将垃圾从堆中移除之后,将剩下的对象向一侧压缩排列,以便移除这些对象之间的空闲空间。由于压缩动作消耗很大,所以应该尽量避免。在4.3.1章对于如何避免压缩有详细的阐述。

压缩是一个非常复杂的过程,因为句柄已经不再存在于虚拟机中了。如果垃圾收集器移动了某个对象,那么需要修改所有指向这个对象的引用。如果有来自栈的引用,那么就无法确定是这个引用确实是对象引用(有可能是一个浮点数),所以这个对象就不能移动。这种对象临时固定在他原来的位置,并且在头信息中使用对应比特位标识。类似的,在JNI操作的过程中,从JNI引用的Java对象也是固化的,无法移动,直到JNI操作结束。利用mptr低三位设置为0,可移动的对象在两个阶段内被压缩到一起。这3个比特位中一个用来标识对象已经被清理,注意,清理标识位出现在两个地方:link域(即OLINK_IsSwapped)和mptr (GC_FirstSwapped)。这两种情况,都会设置最低的比特位(x01)。

在压缩阶段的最后,所有线程通过XM恢复运行。

2 数据区域

2.1 一个对象

image001

图表 1 一个对象

图表1是堆中一个对象的布局

· size + flags (大小和标识)

size + flags在32位架构上占4字节,在64位架构上占8字节,主要目的是用来存储对象的大小。由于对象都是从8字节边界开始,并且对象大小可以被8整除,最低3个比特位不被使用,垃圾收集器用来作为标识位以标识对象的不同状态。另外,由于对象大小是有限的,所以最高2个比特位也用来作为标识位(mptr也是8字节边界的)。

size + flags 中的标识位如下:

§ 第1位有多个用途。在清理阶段作为清理标识,在压缩阶段也会用到它。第1位还是多次固化标识位,用来标识该对象被多次固化。在垃圾收集的一个周期,多次固化标识位被清除,用作其他用途,然后恢复

§ 第2位是dosed位。如果从栈或者寄存器有到某个对象的“引用”,就会在dosed位进行标识。这里“引用”是指在本次垃圾收集周期不能移动该对象,因为垃圾收集器无法确定该“引用”是一个真正指向对象的引用还是碰巧只是一个和对象句柄值相等的整数

§ 第3位是固化标识位。固化的对象就是从堆之外有指向这个对象的引用,例如线程和类对象。此类对象无法移动

§ 32位架构的第31位或者64位架构的第63位是锁(flat lock)竞争位,被锁管理模块(locking, LK)使用

§ 32位架构的第32位或者64位架构的第64位是哈希标识位,表示一个对象已经返回了哈希值。因为对象的哈希值就是对象的地址,如果垃圾收集器移动了这个对象,需要维护这个值

· mptr

mptr槽位在32位架构上占4字节,在64位架构上占8字节,mptr是8字节边界的(译者注:原文为The mptr slot is grained on an 8-byte boundary, not the size + flags. 不知道如何翻译才是准确的),功能为以下两种之一

§ 如果对象不是数组,mptr指向类方法表,垃圾收集器据此找到类信息。通过这种方式,垃圾收集器知道对象是从哪个类实例化的。类的方法表和类本身的信息由类加载器组件(class loader, CL)分配,但是不存储在堆空间

§ 如果对象是数组,mptr是数组内条目的计数

· locknflags

locknflags槽位在32位架构上占4字节,在64位架构上占8字节,但是只有低4字节被使用。主要作用是用来保存锁信息。另外还包含3个标识比特位

§ 第2比特位是数组标识。如果对象是数组,则该标识位设置为1,mptr槽位保存数组中对象的个数

§ 第3比特位是哈希和移动标识位,如果这个标识位被设置为1,表明该对象被移动过,可以在对象在移动之前的位置找到其哈希值

§ (译者注:在图表1中并未画出locknflags槽位,原文如此,笔误?)

· 对象数据

这里是开始记录对象数据的位置

size + flags, mptr以及locknflags一起称为对象头信息

2.2

image003

图表 2 堆

图表2是堆的示意图。堆是虚拟机在初始化时从操作系统申请的一段连续的内存空间。堆底是堆的开始地址,堆顶是堆的结束地址,堆上限是堆中当前使用部分的结束地址。堆上限可以扩展和收缩。-Xmx参数限制了从堆底到堆顶的最大值,如果未设置,默认值如下:

· Xmx

§ Windows: 物理内存的一半,最小16MB,最大2GB-1

§ OS/390 和 AIX: 64MB

§ Linux: 物理内存的一半,最小16MB,最大512MB-1

· Xms

§ Windows, AIX, Linux: 4MB

§ OS/390: 1MB

2.2.1 设置堆大小

对于大多数应用而言,默认的设置即可满足需求。在运行时,堆会自动扩展到一个稳定的状态,保证在任何时刻堆中活动对象占堆大小的70%,在这种状态下,垃圾收集的频率和暂停时间都是可以接受的。

对于某些应用而言,默认参数可能无法保证应用的良好运行,下面列出可能出现的问题以及应对策略。使用verbosegc可以监控堆的使用情况。

· 在堆到达稳定状态之前,垃圾收集发生的过于频繁

使用verbosegc检测堆到达稳定状态时的大小,然后设置-Xms参数等于该大小

· 堆已经扩展到最大限制,但是堆占用率仍然高于70%

增加-Xmx的设置,以使得堆能够扩展到一个能够保证占用率不高于70%的大小。但是需要注意的是,要保证堆的内存都是从物理内存占用,避免出现页交换

· 堆占用率在70%,但是垃圾收集发生的频率过高

修改-Xminf参数。默认是0.3,表示堆要通过扩展保证有30%的空闲空间。例如,设置该参数为0.4,会降低垃圾收集发生的频率

· 暂停时间过长

尝试使用-Xgcpolicy:optavgpause参数。在堆的占用比增高的情况下,该参数能够保证垃圾收集时间的稳定,但是会带来大约5%的吞吐量的下降,具体视应用而定

另外,还有一些小提示:

· 确保堆不会发生页交换(即堆内存全部从物理内存中获取)

· 避免使用finalizer。你不能确保finalizer执行的时机,这样会带来一些问题。在verbosegc的输出中,可以看到是否已经执行了finalizer。如果确实需要使用finalizer,需要注意以下三个关键点

§ 不要在finalizer方法中创建新的对象

§ 不要依赖finalizer来释放一些本地资源

§ 不要在finalizer方法中进行长时间执行的或者阻断式的动作

· 避免压缩。verbosegc的输出中可以显示是否进行了压缩动作。通常,压缩动作是由于大块内存的分配引发的,所以,要分析应用中对于大块内存的需求,比如,一个大的数组对象,可以将其拆分成多个小片段

2.3 分配集合和标识集合

image005

图表 3 堆以及分配集合和标识集合

图表3是堆、分配集合和标识集合的示意图。这两个比特位集合标记了堆中对象的状态。因为堆中对象都是8字节边界的,所以每个比特位对应1个8字节段,这两个比特位集合的大小为堆大小的1/64。

一旦在堆中分配了一个对象,在分配集合中对应对象开始地址的比特位被设置为1。分配集合只是标识了对象被分配,但是无法得知对象是否是活动的。在垃圾收集的标识阶段,标识集合中对应的比特位会被设置,以表示对象是活动的。图表4表示堆中的2个对象,分配集合中对应比特位都被设置为1.

image007

图表 4 堆中一些对象

在标识阶段,Object2是被引用的,Object1是未被引用的,所以在标识集合中对应

Object2的比特位被设置为1,在清理阶段,Object1会被垃圾收集。

2.4 系统堆

image009

图表 5 系统堆

系统堆中包含的对象是跨越整个虚拟机生命周期的对象,通常这些对象是系统级的类对象、可共享的中间对象以及应用级对象。垃圾收集器不会收集系统堆中的对象,因为这些对象在整个虚拟机生命周期内都是可达到的,或者是应用需要共享的一些对象。图表5是系统堆的示意图。系统堆不是连续的存储空间,而是由多个存储段构成的链。系统堆的初始大小在32位架构上是128KB,在64位架构上是8MB。如果系统堆被对象充满,虚拟机会重新分配一块空间,并且加入到系统堆的链表中来。

2.5 空闲链表

image011

图表 6 空闲链表

图表6表示空闲链表,链表的头是一个全局的指针,指向空闲链表的第一个存储段。空闲链表的每个存储段都有一个大小字段和一个指向下一个空闲存储段的指针,链表的最后一个空闲存储段的Next为空指针。

posted on 2008-04-22 11:20 YODA 阅读(3607) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航: