现在的学校真是无聊,今年的毕业生要求每个人必须翻译一篇5000字的英文论文,作为毕设的一部分。虽然可以去网上找有现成翻译的论文,但想想还是自己对自己要负责,加上五一放假三天,也就抽空翻译了一篇六千字的,当作是毕业的一份纪念。加上自己也对JVM的GC机制想来个更深入的了解,所以挑了一篇这方面的论文。翻译完后突发奇想把它贴到blog上来,虽然偶不是专业翻译,但里面内容都是仔细看过的,也查了一些资料、对于很多网上还没有专门中文翻译的术语,也是查了相关英文解释,力求不要曲解作者的意思,都是一个个字码上来的,所以也凝聚了我的人类劳动,呵呵。
论文比较旧,2002年IEEE的,原文题目是《A performance comparison between stop-the-world and multithreaded concurrent generational garbage collection for Java》,主要讲述GC的一种实现,称为MCGC(multithread concurrent garbage collection),翻译完后最大的收获是对JVM的主要GC类型有了大概的了解(截止到2002年)
另外,在IEEE上看到一篇名为《who is collecting your java garbage》的文章,也是2002年的,算是对当时几乎所有流行的jvm的GC机制作了一个基础的解释吧,包括很多比较生僻的名词术语,个人觉得挺好。
先贴出其中几个名词的英文和我的理解,都是网上没有中文解释的,望高手指正:
- sticky-reference-count:这个词让我很头疼,在另一篇英文论文中的解释是:The sticky-reference-count problem refers to the question of how the system could still detect and collect garbage if the reference count is saturated.也就是说对于使用“引用计数”(reference count)算法的GC,如果对象的计数器溢出,则起不到标记某个对象是垃圾的作用了,这种错误称为sticky-reference-count problem,通常可以增加计数器的bit数来减少出现这个问题的几率,但是那样会占用更多空间。一般如果GC算法能迅速清理完对象,也不容易出现这个问题。我最后还是直接按单词翻译了:粘性引用。有点怪怪的
- mutator:这个的英文解释相对比较多,只要是mark-sweep类型的GC都会用到这个术语,我直接使用英文了。mutate的中文是变异,在GC中即是指一种JVM程序,专门更新对象的状态的,也就是让对象“变异”成为另一种类型,比如变为垃圾。
- on-the-fly:这个词不算计算机术语,金山的翻译是“飞行中、闲混”,不过我们这里是用来描述某个GC的类型:on-the-fly reference count garbage collector。其实on-the-fly还有“做某种事情很轻易、很熟练”的意思,我猜是因为此GC不用标记而是通过引用计数来识别垃圾,如某论文上所说,it is the reference count that detects garbage on-the-fly.
- generational gc:这是一种相对于传统的“标记-清理”技术来说,比较先进的gc,特点是把对象分成不同的generation,即分成几代人,有年轻的,有年老的。这类gc主要是利用计算机程序的一个特点,即“越年轻的对象越容易死亡”,也就是存活的越久的对象越有机会存活下去(姜是老的辣)。思前想后,把它翻译为“世代型GC”,呵呵。
下面就是翻译的内容了,直接从word粘贴过来,版式比较乱,懒得改了
stop-the-world与多线程并发世代型这两种java垃圾回收机制的性能对比
Chia-Tien Dan Lo, Witawas Srisa-an, and J.Morris Chang
美国,芝加哥,伊利诺斯技术学院,计算机科学系
{danlo,witty,chang}@charlie.iit.edu
摘要
随着最近java编程语言的流行,动态内存管理技术(例如垃圾收集)的讨论也开始渐入主流。传统垃圾回收技术要么在回收时会导致过长时间的挂起(如stop-the-world STW“标记-收集”算法),要么无法回收循环垃圾(如引用计数)。而世代型垃圾回收技术(generational garbage collection)只是基于“大部分对象都会很快的死亡而被销毁”这样一种世代假设。在本论文中,我们将对一种新型的基于“标记-扫描”、辅以引用计数算法、多线程并发的世代型垃圾回收器(MCGC,multithread concurrent generational garbage collector)作一个性能上的评估。MCGC可以发挥SMP系统上多个CPU和轻量级进程的优势。此外,还可以减少长时间的垃圾回收暂停时间,提高垃圾回收的效率。实验结果表明,比起传统的STW标记扫描垃圾回收器,MCGC在垃圾回收的暂挂时间上有了96.75%的性能提升。此外,MCGC在总的运行时间、内存占用和粘性引用(sticky-reference)计数率方面,花费的时间和内存空间都最少。
索引术语——动态内存管理,OOP,多线程编程,java虚拟机,并行垃圾回收系统,可变伙伴系统,并行垃圾回收器
1. 简介
当关于并发垃圾回收器的讨论在以下文献中【1,4,6,7,8,9,10,11,13,19,16,17】首次被提出来时,很少能够真正实现出来,特别是集成到java虚拟机中【1,6,9,13,16,17】。
首个基于三色提取技术(白,灰,黑)的并行垃圾回收器是由Dijkstra,et al【8】提出的。按照他们的设计,mutator和垃圾回收器可以并行运行。然而,在最初的三色理论中,标记器(marker)必须严格运行于清理器(sweeper)之前,否则,假如标记过程尚未完成(就开始清理)的话,白色对象就会被标记为黑色对象了。这种限制会导致大量的内存占用,因为很多垃圾对象都不能被按时清理。
最近,java并行垃圾回收机制一般都基于引用计数或者标记-扫描。这些成果有:由Bacon和Rajan[1]提出的基于引用计数的回收器Recycler,和Levanoni和Petrank【13】提出的on-the-fly引用计数垃圾回收器。另外还有Domani et al【9,6】提出的基于标记-清理技术的世代型快速垃圾回收器。
虽然Recycler【1】和on-the-fly引用计数(RC)垃圾回收器都是基于引用计数的,但他们之间还是有些许不同。首先,Recycler使用一个mutator缓冲器来解决在更新引用计数时的同步问题;而on-the-fly RC引入了一种滑动视图的方案。其次,Recycler被用在Jalapeno JVM中,而on-the-fly RC用在sun JVM中。第三,Recycler使用了一个特殊的on-the-fly循环检测器,而on-the-fly RC只是通过简单的运行一个marker来收集循环引用的垃圾。第四,Recycler使用哈希表来处理粘性引用计数,以维持真实的引用计数;而on-the-fly RC却是使用marker来记录。
在最近的并行垃圾回收器的发展趋势中,世代型on-the-fly垃圾收集器【9,6】是一种基于Doligez and Leroy [7]等人作品的世代型垃圾回收器。虽然有年轻和年老的世代之分,on-the-fly垃圾收集器并不移动对象。但是,这种收集器只在某些场景下表现的比较好,而不能通用。此外,一些定量性能指标,如最大垃圾回收暂挂时间也没有被公布。
其他作品,如Doligez and Leroy [7]提出的用于ML的多线程世代型垃圾回收器,称之为quasi realtime,可以将两个世代连接起来,其中一个拷贝收集器为年轻世代工作,而marker用于收集年老的世代。所有线程的堆栈和他们的辅助堆都作为年轻世代,而所有线程共享的主堆则作为年老世代。在年轻世代中从拷贝收集器之下存活下来的对象将会被拷贝到主堆中,由标记-清理垃圾回收器将其回收掉。
1998年,由Huelsbergen and Winterbottom提出的一个高度并行化的用于SMLR/IJ系统的垃圾回收器(VCGC),能够并发运行marker、sweeper和mutator。因此,“标记后清理”的限制不复存在了。在这个设计中,有一个周期专门用来区分三色理论中的颜色。通过区分三种周期,一个给marker,一个给mutator,还有一个给sweeper,其中mutator和GC线程可以一起运行。但是,如果某些非固定的垃圾需要被实时清理的话,那并行效果就会大打折扣。
虽然VCGC去除了并行垃圾收集的“标记后清理”的限制,但它不能处理浮动的垃圾,例如在同一个周期内产生的垃圾。由Lo et al. [16, 17]提出的多线程并发垃圾回收器对实时回收浮动垃圾给出了一个解决方案。在该设计中,MCGC用一个2位的计数器来关联并跟踪一个活动对象,因此浮动垃圾不必等到下个周期重新标记后才能清理。此外,粘性引用计数和循环引用也可以在下个周期内被清理,它们本身在实际情况中也很少见。
在这篇论文中,Kaffe JVM 1.06版本中实现了一个STW标记-清理垃圾回收器和MCGC算法,我们将会展示在Linux RedHat v6.2上运行Kaffe JVM 1.06的试验结果,测试平台使用SPECjvm98。2. MCGC算法,version 0
在这一节我们引入一个MCGC算法,MCGC是基于VCGC的垃圾回收算法,并且利用了短生命周期对象的好处。它允许新产生的垃圾在同一个周期内被回收,这是对VCGC作出的一个重大改进。
为了更好的说明MCGC的原理,在version 0中我们只使用一个marker、一个sweeper和一个mutator,同时省略了一些同步,以更好的阐述算法。在version3中,将会描述包含有多个mutator线程的完整版MCGC
Figure 1 The MCGC Version 0
int epoch = 2;
root-set-t roots = {), RS-set = {}, garbage-list = {};
thread-t mutator, marker, sweeper;
thread-create-daemon mutator, marker, sweeper;
loop forever {
thread-resume (mutator (RS-set, COLOR(epoch) ) ;
thread-resume(marker (roots, COLOR(epoch) ) 1 ;
thread-resume(sweeper(COLOR(epoch-2)));
barrier-sync (marker, sweeper) ;
thread-suspend (mutator) ;
while (RS-set) {
thread-resume(marker(RS-set, COLOR(epoch)1);
RS-set = ();
thread-resume(mutator(RS-set, COLOR(epoch)));
barrier-sync (marker) ;
thread-suspend(mutator) ;
}
send the garbage-list to sweeper;
garbage-list = {);
roots = get-roots(mutator);
epoch++;
}
|
2.1 MCGCv0概览
如图1, MCGCv0包含一个marker、一个sweeper和一个mutator。这样,三者之间的同步可以隔离开来,我们可以分别关注mutator和sweeper,mutator和marker,以及sweeper和marker之间的同步。为了同步mutator和marker,我们使用一个重扫描集(RescannerSet,RS)和一个mutator进行关联,RS与VCGC中的存储集(store set)相似。RS_SET用来记录那些由于mutator和marker之间的异步操作而需要重新扫描的对象。在这个设计中,marker和sweeper之间不需要进行同步约束。而mutator与sweeper之间在操作系统空闲列表时,需要同步处理。此外,为了节约线程创建和销毁的时间,所有线程都创建为守护线程(图一,3和4行)。线程一旦被创建,就立刻挂起并等待thread-resume0方法发出的恢复信号。这一线程机制可以简单的通过互斥和条件变量来实现。
MCGCvO与VCGC工作方式相似,不过两个算法也有很多不同之处,比如线程机制、RS_SET的作用、mutator、sweeper、marker,等等。由于VCGC的“初始快照”(snapshot-at-the-beginning)属性,某些在一个周期内产生的不可到达对象无法被收集。虽然可以在下一个周期内被收集,但那样会对内存占用造成很大的影响。要想收集这些浮动垃圾,必须解决两个问题:如何确认垃圾,如何标记它们而不会造成marker的重复工作。算法使用一个2位的引用计数器来确认垃圾。在实际中,该计数器是放入保存颜色的字节中,那样可以节省空间并防止计数器溢出。另一方面,marker在一个周期内递归地确认浮动垃圾,这样可以减少下一周期标记花费的时间。因此在写barrier的过程中,mutator不仅把新对象放入RS中,也把那些由于引用计数而变成垃圾的旧对象放入RS。如果旧对象不是垃圾,那么marker最终也会标记它,因此不必放入RS中。最后,一旦marker和sweeper完成工作后,RS会被重新扫描。(图一,9行)
更新引用计数和把对象放入RS是mutator线程的部分职责。要求只有mutator才能修改引用计数没有任何意义,在MCGCv0中也只有一个mutator线程,因此更新引用计数时不用考虑同步问题。但是在多线程环境下,多个mutator可能会更新同一个引用计数,因此在后续的章节中我们会讨论这个问题。此外,对于mutator(生产者)和marker(消费者)关于RS的同步问题,可以用异步store set【16】或者在特定时间把RS提交给marker(图一,12,13行)来避免。我们的实现很简单,不会造成时间损失,因为在以上两种实现中,mutator都要被挂起。
在传统的世代型垃圾回收器中,RS与年轻世代有着密切的关系。世代型垃圾回收器能够成功很大程度归功于“新创建的对象更易死亡”这一程序行为。因此,我们在一个周期内就试图收集由新创建的对象变成的垃圾。但是在VCGC算法中,新创建的对象被标记为至少存活两个周期,为了解决这个问题,我们的实现中如果新对象变成垃圾,就把新对象放入RS。通过扫描保存了最新对象的RS,不必扫描整个堆就能发现新的垃圾对象。因此,概念上堆被分为两个世代:年老世代(可从根节点到达的对象)和年轻世代(RS中的垃圾)。虽然传统世代型垃圾回收器的内存空间就被划分成generation区和coping区,但MCGCv0在设计上充分利用了这一优点。
只有marker扫描完RS后才会进入下一周期。当marker扫描RS时,mutator依然保持活动,除非marker处理RS时赶上了它,例如当RS为空(图一,11行)。如果mutator在一个周期内要更新大量需要重新扫描的对象的时候,令mutator保持运行可以防止长时间的暂挂。在marker完成RS的工作后,使用一个列表来保存垃圾。这个垃圾列表供sweeper在短时间暂停期间使用(图一,18,19行)。因此,sweeper能够迅速将垃圾列表中的垃圾返还给系统(图一,8行)。注意这里有两种垃圾:一种是通过RS检测到的,另一种是通过普通的三色理论。此外,每次拷贝完整个mutator的根之后,才会进入下一个周期。
3. MCGC:完全版
这个MCGC是多mutator线程版本的MCGCv0,当允许多个mutator的时候,就需要考虑mutator之间的同步了,比如同时更新一个引用计数和修改一个RS。详细的MCGC算法列在图二,其中一个mutator对应一个RS(图二,2行),通过这种RS和mutator的对应方式,上面提到的mutator之间的同步问题就解决了。
由于存在多个mutator,因此在[10]中使用的store set实现就不太适合了,因为它不能处理例如并发插入元素的情况,而我们的实现没有这个问题。但是,是否应该将所有RS一起传递给marker,还是一个个传递,仍然有待研究。在MCGC算法中采用的是后面那种实现(图二,10-18行)。它的好处是mutator不必挂起,marker也可以一个个地检查RS(图二,10行)。其实,RS的检查顺序是任意的,为了减少mutator的挂起时间,较大的RS拥有较高的权限。注意,当marker的处理量追上mutator后,mutator就保持挂起状态了(图二,17行)。
MCGC中的marker扮演了一个很重要的角色,它有三个主要功能:标记活动对象、标记垃圾和更新引用计数。它给活动对象添上mutator“颜色”、利用引用计数来识别垃圾,以及更新引用计数。更新这些集中于RS中引用计数有若干好处:首先,mutator之间的同步问题可以很优雅地得到解决。其次,引用不再是“粘性”的了,因为任何局部引用会被立刻清理掉而不会导致计数器溢出。不过,marker现在只能在更新完所有RS中的引用计数后,才将某个引用计数为零的对象标记为垃圾了,因为可能别的mutator中还包含着指向这个对象的引用。所以,垃圾列表中的某些垃圾可能会在扫描后被重新正名,当检查完所有RS后,垃圾列表中的才是真正的垃圾。这是MCGC中一个比较有趣的特点,但不算是什么问题。
Figure 2 The Multithreaded Concurrent Generational Garbage Collection Algorithm (MCGC)
(1) int epoch = 2;
(2) root-set-t roots = { ) , RS-set[nl = { ) , garbage-list = { ) ;
(3) thread-t mutator [nl , marker, sweeper;
(4) thread-create-daemon mutator[n], marker, sweeper;
( 5 ) loop forever {
(6) thread-resume (mutator In] (RS-set [nl , COLOR (epoch) ) ) ;
(7) thread-resume (marker (roots, COLOR (epoch) ) ) ;
(8) thread-resume(sweeper(COLOR(epoch-2)));
( 9 ) barrier-sync (marker, sweeper) ;
(lo) for i = 1 to n do
(11) thread-suspend(mutator [il ) ;
(12) while (RS-setti]) {
(13) thread-resume (marker (RS-set [il , COLOR(epoch)) ) ;
(14) RS-setli] = { } ;
(15) thread-resume (mutator [il (RS-set [i] , COLOR(epoch) )
} ;
(16) barrier-sync (marker) ;
(17) thread-suspend(mutator[il) ;
(19) send the garbage-list to sweeper; .
(20) garbage-list = ( ) ;
(21) roots = get-roots (mutator tnl ) ;
(22) epoch++;
(23) }
|
4.实现、测试及结果
MCGC已经在Kaffe JVM version 1.0.6中实现了,我们在Intel 奔腾3,650MHZ,Redhat Linux 6.2的平台上运行SPECjvm98 benchmark得到实验结果。所有基准程序都在100的最大量上运行。运行结果显示,MCGC比起传统的STW垃圾回收算法多出了1052的增速,垃圾收集的暂停时间也少了8.8毫秒,此外在每个周期,MCGC能检测到2806个垃圾对象,这些垃圾对象在并行VCGC中是当作浮动垃圾的。
4.1 基准
我们从SPECjvm98选取7个java程序来测试MCGC的性能。这些基准程序使用了以下一些指标来测量JVM的性能,如:大批量字节码(high byte-code content),大规模循环,可重复性,堆的使用和分配速率,以及在参考平台上的I-Cache和D-Cache的失效率。大部分程序包括整型和浮点运算,库调用,或者I/O操作。不过SPECjvm98不包含AWT、网络和图形应用程序。此外,在这个测试套件中,只有mtrt才是多线程的。
关于SPECjvm98的完整研究是由Dieckmann and Holzle [5]完成的,在他们的文章中,列出了包括对象生命期、对象大小分布、类型分布和对象排列的花费等内容,此外在【14,15】中还提到了系统级的性能研究。Table 1汇总了关于SPECjvm98中基准程序的描述。
4.2 实现
MCGC的实现是基于Kaffe GC的,下面是它的总体设计思路。
Kaffe JVM 1.0.6的GC系统是基于STW标记-清理垃圾回收算法的,并且使用隔离列表分配模式【21】。体积大于一个内存页的称为大对象,小于一个内存页的则为小对象。所有的小对象分配请求都会被四舍五入为一个大小最接近的对象体积,该对象体积由JVM在启动时预先指定好。Kaffe GC系统为每个GC块维持一个空余列表,其中包含了相等大小的特定对象。每个GC块分配到一个内存页,一旦找到一个包含所请求对象大小的GC块,就可以通过特定的索引技术将该空余对象释放掉。同时,Kaffe GC使用三色理论来维持GC对象的状态。如果请求一个大对象,Kaffe会将其四舍五入至页大小的边界,即为该对象分配一个页的整数倍的空间。
下面是MCGC实现中用到的数据结构。首先,每个对象会拥有以下六种颜色之一:正在使用(inuse)、空余free、固定fixed、marker、sweeper、mutator。除了固定,其他颜色都是可以自解释的,而固定则表明该固定对象不需要被GC回收。其次,每个对象有一个2位的引用计数器。注意,颜色和引用计数器信息是编码到同一个字节中的。第三,总共有五个列表,finalise-list, sweeper-list, marker-list, mutator-list, makrer-live-list and garbage-list。每个列表都是一个双向链表,用于保存各个垃圾回收周期的对象。finalise-list列表保存需要被析构的对象,garbage-list用来给marker在一个回收周期内回收垃圾。此外,sweeper-list, marker-list, mutator-list 则包含对应颜色的对象。第四,每个页都包含一个头,用来描述诸如对象大小、空余对象数量、第一个空余对象的地址、颜色、引用计数等GC块的信息。把每个块都分为等大小的对象是毫无意义的,我们使用的是隔离列表机制。
每个对象都被分配到一种mutator颜色,并附加到对应的mutator-list中。为了避免并发访问同一个mutator-list,每个mutator都各自有一个mutator-list。不过,在mutator-list中的对象也可能会变成垃而被marker从列表中删除掉。这个同步过程可以在删除列表的头对象上加一个锁,删除过程是安全的,因为mutator只会往头对象后面插入对象。另一方面,mutator会更新记录对象的引用计数,并重新扫描将相应的对象放入RS中给marker处理。
Marker从根集中开始它的标记过程(图二,7行),从marker-list中将活动对象移至makrer-live-list中。其中,makrer-live-list和marker-list只由marker操作,所以不存在同步问题。于此同时,sweeper将对象清理回系统列表(图二,8行)。Sweeper会扫描整个sweeper-list的对象,将对象的锁和bins合在一起供mutator使用。sweeper-list只对sweeper可见,也不存在同步问题。此外,因为不用扫描整个堆而只需扫描sweeper-list,整个过程不会花很多时间。
当marker和sweeper完成后(图二,9行),mutator被逐个挂起,系统开始检查他们的RS,包括更新引用计数、重新扫描活动对象或垃圾。Mutator被挂起时,它的RS被传递给marker的一个局部RS结构(图二,13、14行),在恢复mutator之前,RS被设为无效null(图二,14行)。然后marker开始标记局部RS中的活动对象,将其放入makrer-live-list中,再把垃圾放入garbage-list中。由于局部RS、garbage-list、makrer-live-list都只由marker操作,不需要同步。最后,当RS为空的时候mutator挂起,而所有RS都为空时,这个周期也就结束了。由于在内存分配上,一个字是4个字节,所以把RS简单实现为单字结构、最小两位存储引用计数的做法其实没什么意义。
检查RS的过程中(图二,10-18行),garbage-list中的某个垃圾对象可能会由于其他地方对引用计数的异步更新而不再是垃圾对象,不过这也不是个问题,因为marker可以将该对象从garbage-list取出,不需要任何同步地附加到makrer-live-list中。这个过程完成之后,整个garbage-list就被附加到sweeper中,然后garbage-list被空值化(图二,19-20行)。这时所有的mutator都已被挂起。最后,根集被拷贝以准备进入下一个周期。
我们实现了一个写阻碍过程(write barrier routine),当mutator更新引用结构图时,用来记录新旧引用。这个过程会被以下java字节码指令调用:PUTFIELD, PUTSTATIC和AASTORE。PUTFIELD指令用来设置对象的一个域,当该域是引用类型时,该过程就会被调用;PUTSTATIC和PUTFIELD类似,只是用在类的静态域中;AASTORE用于引用数组类型。
4.3 实验结果
我们在Intel 奔腾3,650MHZ,Redhat Linux 6.2的平台上运行SPECjvm98 benchmark,对Kaffe JVM 1.0.6进行测试,包括MCGC和STW标记-清理两种GC,得到实验结果。表二列出了所有周期中检测RS得到的最大浮动垃圾数,以及粘性引用数统计。结果表明一个周期内产生了多达7251个浮动垃圾。也就是说,换成VCGC算法的话,会有7251个浮动垃圾对象无法即时清除,内存占用也就会增加,这也表明了MCGC相对于VCGC的优势所在。另一方面,粘性引用率也小于16.09%,大部分情况是小于6.63%,说明2bit的引用计数器已经足够。
5 结论
当前,针对“并行垃圾收集”人们已经做了很多研究,例如世代型并行垃圾收集器、高度并行化“标记-清理”垃圾回收器,以及硬件辅助的垃圾回收器,最终目的就是为了减少收集垃圾造成的暂停时间。这篇论文中我们研究了MCGC算法的性能,由于它的高效性,MCGC可以被整合进硬件垃圾回收系统中,例如可修改的伙伴系统【2,3】,或者是其他需要标明活动对象的垃圾回收机制中,例如硬件辅助的实时垃圾回收系统【18】。
MCGC算法已经被整合进Kaffe JVM 1.0.6中,我们在Intel 奔腾3,650MHZ,128M内存,Redhat Linux 6.2的平台上运行SPECjvm98 benchmark。结果表明MCGC的表现远超过VCGC,在一个周期内通过检查RS发现了多达2806个垃圾对象。此外,比起传统的STW“标记-清理”垃圾回收器,MCGC也有着更好的性能,同时MCGC的最大垃圾回收暂挂时间优化了96.75%,平均暂挂时间也优化了95.66%。最后,在总体运行时间、内存占用和粘性引用率上,MCGC的时间和空间占用最小。
6. 参考
略