译注:
作者:Nate Berkopec,来自speedshop
概要:你有没有想过 Ruby 的 GC 是如何工作的?让我们看看我们能从 Ruby 为我们提供的
GC.stat
哈希里学到什么。
大多数的 Ruby 程序员都不清楚垃圾回收在运行时是如何工作的:什么触发了它,它运行得多频繁,它收集了什么以及不收集什么。这不完全是个坏事:动态语言(例如 Ruby)中的垃圾回收非常的复杂,而程序员最好只关注编写对用户重要的代码。
但是,偶尔的,你会被 GC 给懵了:它可能运行得太频繁或者又不够,也可能是你的程序使用了大量的内存但你却不知为何。也可能你只是想知道 GC 是如何工作的。
我们学习 CRuby(用 C 写的标准 Ruby 运行时)关于垃圾回收的一个方法是看看它内建的GC
模块。如果你还没读过这个模块的文档,那得读一下,它有几个有意思的方法。但现在,我们只看这一个方法:GC.stat
。
GC.stat
输出一组不同数字的哈希,但它们并没有被良好的文档描述,并且其中一些非常容易让人混淆,除非你阅读了相关的 C 代码!不过我帮你读了,现在一起看看GC.stat
提供的信息吧。
这是我在一个用 Ruby-2.4.0 刚刚启动的irb
会话中执行GC.stat
的输出:
{
:count=>15,
:heap_allocated_pages=>63,
:heap_sorted_length=>63,
:heap_allocatable_pages=>0,
:heap_available_slots=>25679,
:heap_live_slots=>25506,
:heap_free_slots=>173,
:heap_final_slots=>0,
:heap_marked_slots=>17773,
:heap_eden_pages=>63,
:heap_tomb_pages=>0,
:total_allocated_pages=>63,
:total_freed_pages=>0,
:total_allocated_objects=>133299,
:total_freed_objects=>107793,
:malloc_increase_bytes=>45712,
:malloc_increase_bytes_limit=>16777216,
:minor_gc_count=>13,
:major_gc_count=>2,
:remembered_wb_unprotected_objects=>182,
:remembered_wb_unprotected_objects_limit=>352,
:old_objects=>17221,
:old_objects_limit=>29670,
:oldmalloc_increase_bytes=>46160,
:oldmalloc_increase_bytes_limit=>16777216
}
注:Ruby-2.5.1 的输出是一样的
OK,很多东西,这有 25 个没有文档的键值。
首先,我们看看 GC counts
:
{
:count=>15,
# ...
:minor_gc_count=>13,
:major_gc_count=>2
}
这几个非常直接。minor_gc_count
和major_gc_count
是 Ruby 进程启动之后的各类型 GC 运行次数。万一你不知道,自从 Ruby-2.1 开始就有个2种垃圾回收,major 和 minor。minor GC 只尝试回收「新」的对象——存活时间小于等于 3 次 GC 周期。而 major GC 会尝试回收所有对象,甚至是存活时间超过 3 次 GC 周期。count
= minor_gc_count
+ major_gc_count
。如果想了解更多,可以参考我在 FOSDEM 上关于the history of Ruby Garbage Collection的讲解。
出于几个原因跟踪 GC 次数会是有用的。例如,某个特定的后台任务总是触发 GC(以及触发了多少次)。例如,这是一个 Rack 中间件可以记录当服务器处理一个请求时的 GC 次数变化:
class GCCounter
def initialize(app)
@app = app
end
def call(env)
gc_counts_before = GC.stat.select { |k,v| k =~ /count/ }
@app.call(env)
gc_counts_after = GC.stat.select { |k,v| k =~ /count/ }
puts gc_counts_before.merge(gc_counts_after) { |k, vb, va| va - vb }
end
end
如果你的服务是多线程的,那么这个数据不会 100% 的准确,因为其他的线程弄出来的内存压力也可能触发 GC,但这是一个入口。
现在,我们继续看堆数量
(heap numbers
)的统计:
{
# Page numbers
:heap_allocated_pages=>63,
:heap_sorted_length=>63,
:heap_allocatable_pages=>0,
# Slots
:heap_available_slots=>25679,
:heap_live_slots=>25506,
:heap_free_slots=>173,
:heap_final_slots=>0,
:heap_marked_slots=>17773,
# Eden and Tomb
:heap_eden_pages=>63,
:heap_tomb_pages=>0
}
在这里,堆heap
是一个 C 数据结构,有时也称为对象空间ObjectSpace
,在其中我们保持了对当前所有活 Ruby 对象的引用,每一个 堆 heap 页面 page 包含了大约 408 个槽 slots,每个 槽 slot 包含了一个活的 Ruby 对象的信息。
heap_allocated_pages
是当前已分配的堆空间的数字,这些页面 pages 可能完全是空的、完全满的、或者是中间状态。heap_sorted_length
是内存中堆的实际大小 —— 如果我们有 10 个堆页面,然后 free 了中间某个页面,那么 堆页面 的 长度length
仍然是 10(因为我们没法在内存中移动页面)。heap_sorted_length
总是大于等于实际分配的页面数。heap_allocatable_pages
—— 这是 Ruby 当前拥有的 堆页面大小 的一些(已经分配的 malloced
)内存块,我们可以分配一个新的堆页面。如果 Ruby 需要为增加的对象分配新的堆页面,那就会首先使用这个空间。OK,现在我们拿到了一堆和单个对象的槽 slots有关的数字。heap_available_slots
,很明显是堆页面中所有槽的数量 —— GC.stat[:heap_allocated_pages]
恒等于 GC.stat[:heap_available_slots]
/ GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT]
。然后:
heap_live_slots
是活跃对象数;heap_free_slots
是堆页面中空的槽;heap_final_slots
是被析构函数 finalizers附着了的对象槽。析构函数是 Ruby 中一类朦胧的特性 —— 它们是对象将被释放时运行的 Procs。例如:ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)
heap_marked_slots
几乎是旧对象(存活超过 3 次 GC 周期的对象)的数量加上写屏障无保护对象 write barrier unprotected objects(一会细说)的数量。注:上面两段列表,在原文中是段落形式,但是弄出列表形式似乎好点。
至于 GC.stat
中槽数量的实际使用,如果你遇到内存膨胀问题的话我建议监控heap_free_slots
。大量的空对象槽(free slots???)通常预示着你有几个 actions 分配了大量的对象然后又释放了它们,这会不停地增加你的 Ruby 进程的内存。若想了解修复这问题的更多技巧,参考我在 Rubyconf 上关于 Ruby 内存问题的演讲。
现在,我们有tomb_pages
和eden_pages
,eden_pages
是包含至少一个活对象的堆页面。tomb_pages
不包含活对象,所以有完全空的对象槽 (free slots???)。Ruby 运行时只释放 tomb_pages 返回操作系统,而eden_pages
则永远不被 free。
简单的,还有几个累积的 已分配/已 free 数字
{
:total_allocated_pages=>63,
:total_freed_pages=>0,
:total_allocated_objects=>133299,
:total_freed_objects=>107793
}
这些数字是进程整个生命周期的累积值 —— 它们不会被重置、减小。根据它们的变量名称就已经很好理解了。
最后,我们还有 垃圾回收阈值 garbage collection thresholds:
{
:malloc_increase_bytes=>45712,
:malloc_increase_bytes_limit=>16777216,
:remembered_wb_unprotected_objects=>182,
:remembered_wb_unprotected_objects_limit=>352,
:old_objects=>17221,
:old_objects_limit=>29670,
:oldmalloc_increase_bytes=>46160,
:oldmalloc_increase_bytes_limit=>16777216
}
呃,Ruby 开发者的一个主要误解是 GC*何时*被触发。我们可以通过GC.start
手动触发 GC,但这不发生于产品线。一些人觉得 GC 是根据某种定时器(例如每隔几秒或几个请求)来运行的,事实并非如此。
free_slots
剩下时,Ruby 运行 minor GC —— 标记并且清除所有新对象和记忆集中未被写屏障保护的对象。这些术语随后有解释。GC.stat
包含了这四个阈值(限制)和运行时的当前状态。
malloc_increase_bytes
指的是 Ruby 为「堆(我们已经讨论过的)」外 对象分配的空间。堆页面里的每一个对象槽只有 40 字节(参考GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
),那当对象大小超过 40 字节(比如长字符串)时会发生什么呢?我们为这个对象在其他任何地方malloc
空间!如果我们为一个字符串分配了 80 字节的空间,那 malloc_increase_bytes
就将增加 80。当这个数值抵达了限制,就触发了一次 major GC。oldmalloc_increase_bytes
和 malloc_increase_bytes
类似,但只针对 old 对象。remembered_wb_unprotected_objects
是已记忆集 remembered set中的部分但没有被写屏障 write-barrier保护 的对象数量。
old_objects
被标记为 老 对象槽的数量。如果你的问题是 major GC 的次数过多,那追踪这些阈值可能对 debug 有帮助。
我希望这是对 GC.stat 的教育看法 —— 它是个信息丰富的哈希,能用来在需要修复不良 GC 行为时构建临时的 debug 方案。
译注:添加了一个 gc+grouped_stat.rb 文件,可以简单看看
不是非常有把握的翻译:
malloc_increase_bytes
refers to when Ruby allocates space for objects *outside *of the “heap” we’ve been discussing so farmalloc_increase_bytes
指的是 Ruby 为「堆(我们已经讨论过的)」外 对象分配的空间remembered_wb_unprotected_objects
is a count of objects which are not protected by the write-barrier
and are part of the remembered setremembered_wb_unprotected_objects
是已记忆集 remembered set中的部分但没有被写屏障 write-barrier保护 的对象数量。GC.stat
包含了这四个阈值(限制)和运行时的当前状态