Ruby 2.1: RGenGC 已翻译 100%

oschina 投递于 2013/12/30 17:57 (共 7 段, 翻译完成于 01-09)
阅读 1231
收藏 31
3
加载中

Ruby 2.1添加了一个“受限的”分代收集器,标记时间更短,大大减少了垃圾回收的耗时。

让我们看看Rugby 垃圾回收的发展历程。

Ruby 1.8: 简单标记和清扫

经典的标记和清扫实施过程。在两个实施阶段整个世界都静止了。

  1. 从根遍历对象图并标记活动对象,使用对象结构内的一个位(FL_MARK)。

  2. 遍历所有的堆插槽,并向空闲列表中添加未标记的插槽。

Ruby 1.9.3:lazy weep

@nari3 增加 LazySweepGC, 将垃圾收集减少的标记阶段。 由于需要对象插槽,所以堆区被逐渐清扫。

A
AmyDevil
翻译于 2014/01/04 18:44
1

Ruby 2.0:使用位图支持写时复制安全

@nari3 添加了位图标记垃圾回收机制,能够帮助Unix系统在子进程间共享内存。标记阶段也重写为非递归方式

虽然通过位图机制节省的内存很少,但是这个修补释放了一个bit位(FL_MARK之后变成了FL_WB_PROTECTED)并为实现分代回收器打下了基础。

Ruby 2.1:老生代和次标记

@ko1 设计了 RGenGC,一个能够递进实现和支持C扩展的分代回收器。

在堆中的对象现在被分成了两类:

  • 受写屏障保护的对象(FL_WB_PROTECTED)

  • 非保护的对象(或者叫“shady”对象)

    • 没有写屏障(例如Proc, Ruby::Env)

    • C扩展访问不安全(例如RARRAY_PTR, RSTRUCT_PTR)

只有受保护对象能够被提升为老生代。(这一点在RGenGC中是严格限定的)

非保护对象不能被提升,但是如果被老生代对象引用,非保护对象会被添加到一个记忆区。次标记过程能够快这么多就是因为只需要从记忆区开始遍历引用。

TX
TX
翻译于 2014/01/08 17:44
1

堆布局

Ruby对象都是存放在ruby堆中的,而且被划分成了很多页。每一页大小为16KB,具有大约408个对象插槽。

在页中,一个RVALUE插槽占据40字节空间,字符串占据少于23字节的空间,而少于4个元素的数组则能够存放在40字节的插槽中。

GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]    #=> 每个对象插槽40字节(64位系统)
GC::INTERNAL_CONSTANTS[:HEAP_OBJ_LIMIT] #=> 每个堆页408个插槽

堆中的页只会存在以下两个区域中的一个:eden(伊甸)和tomb(坟墓)。Eden区中的页存放着活动对象,而tomb区中的是空白页没有对象存放。这两个区的页合在一起就是ruby堆的总大小。

GC.stat(:heap_used) ==
  GC.stat(:heap_eden_page_length) + GC.stat(:heap_tomb_page_length)

在延迟清除时eden区中的页每次被清除一个。每一页都能够提供最多408个对象插槽进行重新使用。在延迟清除结束时,eden区所有没被标记的插槽都会被新的对象替代。

在清除阶段发现的空白页会被移动到tomb区。这样能够减少eden的存储碎片(当然会先使用稀疏页填充),而且使得tomb区能在被使用前伸缩容量。

一旦eden区的对象插槽不够用,在tomb去中的空白页就会被重新移动到eden区。这个过程是递进的,每次移动一页。当tomb区中没有页时,一次标记过程会被触发,然后一次新的循环重新开始。

TX
TX
翻译于 2014/01/08 18:05
1

主清除 vs 次清除

举个例子,让我们来看看github的大型rails应用。

首先,我们会计算有多少长期活动的对象在应用启动之后被创建。

# 加载controllers/models和其他代码
GitHub.preload_all

# 在一次主清除和全清除后计算堆状态
GC.start # same as GC.start(full_mark: true, immediate_sweep: true)

# three ways to measure live slots
# 使用三种方法去计算活动插槽数量
count = ObjectSpace.count_objects
count[:TOTAL] - count[:FREE]        #=> 565121

GC.stat(:heap_live_slot)            #=> 565121

GC.stat(:total_allocated_object) -
GC.stat(:total_freed_object)        #=> 565121

可以看到有大约565千个长期活动的启动对象,大约95%被提升为老生代。

s = GC.stat
100.0 *              s[:old_object] / s[:heap_live_slot]  #=> 94.90
100.0 * s[:remembered_shady_object] / s[:heap_live_slot]  #=>  1.88

这意味着在堆中仅只有大约5%的对象需要在次清除中被遍历,以大约2%的记忆区对象为引用起点。

就像我们期望的,这样可以使得次清除的停顿变得非常非常短:在我们的应用中比花费58毫秒的主清除相比只需要7毫秒。

time{ GC.start(full_mark:  true, immediate_sweep: false) }  #=> 0.058
time{ GC.start(full_mark: false, immediate_sweep: false) }  #=> 0.007

在代码执行时多数清除时的停顿都会是次清除,完成得也很快。经过如此,经过一段时间后记忆区和老生代的占用空间也会增加。如果其中任何一个的容量达到了限定的两倍,一次主清除就会被触发以重置空间。

>> GC.stat.values_at(:remembered_shady_object, :old_object)
=> [10647, 536785]

>> GC.stat.values_at(:remembered_shady_object_limit, :old_object_limit)
=> [21284, 1073030]

主清除和次清除的频率也需要监控到。比如说你可能想要绘制一幅图来显示“每请求的主GC次数”,“每请求的次GC次数”,“每主GC的次GC次数”。

TX
TX
翻译于 2014/01/08 20:12
1

参数调优

在上面讲到的app中,我们使用的是如下的GC设置:

export RUBY_GC_HEAP_INIT_SLOTS=600000
export RUBY_GC_HEAP_FREE_SLOTS=600000
export RUBY_GC_HEAP_GROWTH_FACTOR=1.25
export RUBY_GC_HEAP_GROWTH_MAX_SLOTS=300000

  • RUBY_GC_HEAP_INIT_SLOTS:在堆中的初始插槽数 (默认: 10000)
    我们的应用启动时大概会生成600千个长期活动对象,所以我们将此参数设置为600千以减少启动时的GC活动。

  • RUBY_GC_HEAP_FREE_SLOTS:预留给清除重用的空余插槽最小值(默认:4096)
    我们的服务器配有额外的RAM,所以我们将这个参数调得很高,用内存换取两次GC间的时间。每次请求平均会分配75千个对象,所以600千个空余插槽可以在两次标记停顿之间满足大概8个请求。

  • RUBY_GC_HEAP_GROWTH_FACTOR:堆增长的系数 (默认:1.8倍)
    既然在使用以上的配置后我们的堆已经足够大,我们就将增长系数调低到1.25倍,让插槽数的增量缩小一些。

  • RUBY_GC_HEAP_GROWTH_MAX_SLOTS:增加空白插槽的最大值 (默认:无限制)
    除了减少堆增长系数,我们也给每次堆中能增加的空白插槽数量设置了上限,设定为最大300千个。

TX
TX
翻译于 2014/01/09 11:00
1

malloc() 的限制

之前说到,每个Ruby对象在eden区的堆中占用40字节的空间。

当一个对象需要更多的空间时,它就会从常规的进程堆中收集内存空间(通过ruby_xmalloc()包装器)。例如,当一个字符串增长至大于23字节时,它会为自己收集一个独立的更大的缓存。这个字符串(或者任何其他对象)使用的额外内存都能使用objspace.so中的ObjectSpace.memsize_of(o)方法获取到。

在内部Ruby虚拟机一直跟踪着malloc_increase这个参数,这是指已经收集到但是还未释放的字节数。这个参数实际上就是进程的内存增长量。当多余16MB的内存被添加时,会强制执行一次GC,即使空白插槽仍然够用。这个限制一开始设定为16MB,但是会逐步适应你代码中的内存使用模式。

初始值、最大值和动态增长系数都能够通过环境变量控制:

  • RUBY_GC_MALLOC_LIMIT:(默认: 16MB)

  • RUBY_GC_MALLOC_LIMIT_MAX:(默认: 32MB)

  • RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR:(默认: 1.4x)

malloc增长和限制值都能够通过GC.stat监视到:

>> GC.stat.values_at(:malloc_increase, :malloc_limit)
=> [14224, 64000000]

>> GC.stat.values_at(:oldmalloc_increase, :oldmalloc_limit)
=> [20464, 64000000]

在我们的应用中我们将初始值提高到64MB,以达到减少启动和内存使用峰值时GC次数的目的。

export RUBY_GC_MALLOC_LIMIT=64000000
export RUBY_GC_OLDMALLOC_LIMIT=64000000

TX
TX
翻译于 2014/01/09 11:19
1

GC事件

最后,ruby2.1交付了新的能够监视运行时GC的监视点。它们都通过C实现的rb_tracepoint_new()提供。

RUBY_INTERNAL_EVENT_GC_START

RUBY_INTERNAL_EVENT_GC_END_MARK
RUBY_INTERNAL_EVENT_GC_END_SWEEP

使用这些事件的C扩展也能从rb_gc_stat()和rb_gc_latest_gc_info()获得好处,这两个函数提供了对GC.stat和GC.latest_gc_info的安全访问。

Ruby2.2及以后

通过上面对RGenGC的介绍,可以看出Ruby2.1中ruby的GC有了一次重大的提升。7毫秒级别的次标记和95%的老生代提升都是让人印象深刻的成果。更别提我们所有的C扩展一个都不用修改。向@ko1脱帽致敬!

Ruby2.2会将GC算法由两代扩展至三代。(实际上,2.1已经包含一个RGENGC_THREEGEN的编译标志来启用三代回收算法)。@ko1同时也打算实现增量标记停顿,这能消除对主清除时的长停顿的需要。

TX
TX
翻译于 2014/01/09 11:30
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(6)

aiasfina
aiasfina

引用来自“Rubyfans”的评论

引用来自“阿昭”的评论

虽然2.1的GC看起来很NB的样子
但我在生产中已经用血和泪证明了,现在还是不要用

ruby2.1刚发布就用上了,就这个站www.yihub.com,感觉不错!不知道2.1在您那边有什么问题。

Github 的程序员列出的buglist:https://gist.github.com/tmm1/8393897
Rubyfans
Rubyfans

引用来自“阿昭”的评论

虽然2.1的GC看起来很NB的样子
但我在生产中已经用血和泪证明了,现在还是不要用

ruby2.1刚发布就用上了,就这个站www.yihub.com,感觉不错!不知道2.1在您那边有什么问题。
FutureTime
FutureTime

引用来自“阿昭”的评论

虽然2.1的GC看起来很NB的样子
但我在生产中已经用血和泪证明了,现在还是不要用

往往理论设计和是实际实现是有差别的
阿昭
阿昭
虽然2.1的GC看起来很NB的样子
但我在生产中已经用血和泪证明了,现在还是不要用
sevk
sevk
学习了
crossmix
crossmix
god,a smile message for you
返回顶部
顶部