Ruby 2.1添加了一个“受限的”分代收集器,标记时间更短,大大减少了垃圾回收的耗时。
让我们看看Rugby 垃圾回收的发展历程。
经典的标记和清扫实施过程。在两个实施阶段整个世界都静止了。
从根遍历对象图并标记活动对象,使用对象结构内的一个位(FL_MARK)。
遍历所有的堆插槽,并向空闲列表中添加未标记的插槽。
@nari3 增加 LazySweepGC, 将垃圾收集减少的标记阶段。 由于需要对象插槽,所以堆区被逐渐清扫。
@nari3 添加了位图标记垃圾回收机制,能够帮助Unix系统在子进程间共享内存。标记阶段也重写为非递归方式。
虽然通过位图机制节省的内存很少,但是这个修补释放了一个bit位(FL_MARK之后变成了FL_WB_PROTECTED)并为实现分代回收器打下了基础。
@ko1 设计了 RGenGC,一个能够递进实现和支持C扩展的分代回收器。
在堆中的对象现在被分成了两类:
受写屏障保护的对象(FL_WB_PROTECTED)
非保护的对象(或者叫“shady”对象)
没有写屏障(例如Proc, Ruby::Env)
C扩展访问不安全(例如RARRAY_PTR, RSTRUCT_PTR)
只有受保护对象能够被提升为老生代。(这一点在RGenGC中是严格限定的)
非保护对象不能被提升,但是如果被老生代对象引用,非保护对象会被添加到一个记忆区。次标记过程能够快这么多就是因为只需要从记忆区开始遍历引用。
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区中没有页时,一次标记过程会被触发,然后一次新的循环重新开始。
举个例子,让我们来看看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次数”。
在上面讲到的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千个。
之前说到,每个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
最后,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的安全访问。
通过上面对RGenGC的介绍,可以看出Ruby2.1中ruby的GC有了一次重大的提升。7毫秒级别的次标记和95%的老生代提升都是让人印象深刻的成果。更别提我们所有的C扩展一个都不用修改。向@ko1脱帽致敬!
Ruby2.2会将GC算法由两代扩展至三代。(实际上,2.1已经包含一个RGENGC_THREEGEN的编译标志来启用三代回收算法)。@ko1同时也打算实现增量标记停顿,这能消除对主清除时的长停顿的需要。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务