在 Ruby 中查找内存泄漏的原因

Posted

技术标签:

【中文标题】在 Ruby 中查找内存泄漏的原因【英文标题】:Finding the cause of a memory leak in Ruby 【发布时间】:2013-12-21 13:17:00 【问题描述】:

我在我的 Rails 代码中发现了内存泄漏 - 也就是说,我发现了 what 代码泄漏,但没有发现 为什么 它泄漏。我已将其简化为不需要 Rails 的测试用例:

require 'csspool'
require 'ruby-mass'

def report
    puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#$$"`.strip.split.map(&:to_i)[1].to_s + 'KB'
    Mass.print
end

report

# note I do not store the return value here
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))

ObjectSpace.garbage_collect
sleep 1

report

ruby-mass 据说可以让我看到内存中的所有对象。 CSSPool 是一个基于 racc 的 CSS 解析器。 /home/jason/big.css 是a 1.5MB CSS file。

这个输出:

Memory 9264KB

==================================================
 Objects within [] namespace
==================================================
  String: 7261
  RubyVM::InstructionSequence: 1151
  Array: 562
  Class: 313
  Regexp: 181
  Proc: 111
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 29
  Gem::Requirement: 25
  RubyVM::Env: 11
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Memory 258860KB

==================================================
 Objects within [] namespace
==================================================
  String: 7456
  RubyVM::InstructionSequence: 1151
  Array: 564
  Class: 313
  Regexp: 181
  Proc: 113
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 30
  Gem::Requirement: 25
  RubyVM::Env: 13
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

您可以看到内存在方式上升。一些计数器上升,但不存在特定于 CSSPool 的对象。我使用 ruby​​-mass 的“索引”方法来检查具有如下引用的对象:

Mass.index.each do |k,v|
    v.each do |id|
        refs = Mass.references(Mass[id])
        puts refs if !refs.empty?
    end
end

但同样,这并没有给我任何与 CSSPool 相关的信息,只是 gem 信息等。

我也试过输出“GC.stat”...

puts GC.stat
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))
ObjectSpace.garbage_collect
sleep 1
puts GC.stat

结果:

:count=>4, :heap_used=>126, :heap_length=>138, :heap_increment=>12, :heap_live_num=>50924, :heap_free_num=>24595, :heap_final_num=>0, :total_allocated_object=>86030, :total_freed_object=>35106
:count=>16, :heap_used=>6039, :heap_length=>12933, :heap_increment=>3841, :heap_live_num=>13369, :heap_free_num=>2443302, :heap_final_num=>0, :total_allocated_object=>3771675, :total_freed_object=>3758306

据我了解,如果一个对象没有被引用并且发生垃圾回收,那么应该从内存中清除该对象。但这似乎不是这里发生的事情。

我还阅读了有关 C 级内存泄漏的信息,并且由于 CSSPool 使用使用 C 代码的 Racc,我认为这是一种可能性。我已经通过 Valgrind 运行了我的代码:

valgrind --partial-loads-ok=yes --undef-value-errors=no --leak-check=full --fullpath-after= ruby leak.rb 2> valgrind.txt

结果为@​​987654325@。我不确定这是否证实了 C 级泄漏,因为我还读到 Ruby 使用 Valgrind 不理解的内存进行操作。

使用的版本:

Ruby 2.0.0-p247(这是我的 Rails 应用运行的) Ruby 1.9.3-p392-ref(用于使用 ruby​​-mass 进行测试) 红宝石质量 0.1.3 CSSPool 4.0.0 来自here CentOS 6.4 和 Ubuntu 13.10

【问题讨论】:

我使用 1.9.3.x 在我的机器上重现了同样的问题。你用的是什么版本? 我已在问题中添加了版本信息。 【参考方案1】:

您似乎正在这里进入失落的世界。我认为问题也不在于racc 中的 c 绑定。

Ruby 内存管理既优雅又麻烦。它将对象(名为RVALUEs)存储在所谓的中,大小约为16KB。在底层,RVALUE 是一个 c-struct,包含不同标准 ruby​​ 对象表示的union

因此,堆存储 RVALUE 对象,其大小不超过 40 字节。对于StringArrayHash 等对象,这意味着小对象可以放入堆中,但一旦达到阈值,就会分配 Ruby 堆之外的额外内存。

这个额外的内存是灵活的;一旦对象被 GC 处理,is 就会被释放。这就是为什么你的测试用例 big_string 显示内存上下行为:

def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#$$"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end
report
big_var = " " * 10000000
report
big_var = nil 
report
ObjectSpace.garbage_collect
sleep 1
report
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

但堆(请参阅GC[:heap_length])本身不会释放回到操作系统,一旦获得。看,我将对你的测试用例做一个简单的改动:

- big_var = " " * 10000000
+ big_var = 1_000_000.times.map(&:to_s)

然后,瞧:

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

内存不再释放回操作系统,因为我引入的数组的每个元素适合RVALUE 大小并且存储在 ruby​​ 堆中。

如果您在 GC 运行后检查 GC.stat 的输出,您会发现 GC[:heap_used] 的值按预期减少。 Ruby 现在有很多空堆,准备好了。

总结:我不认为,c 代码泄漏。我认为问题在于css 中巨大图像的 base64 表示形式。我不知道解析器内部发生了什么,但看起来巨大的字符串迫使 ruby​​ 堆计数增加。

希望对您有所帮助。

【讨论】:

我想我得再研究一下解析器,看看我是否能找到它的代码的哪一部分导致了这种行为,以及是否可以重写它来避免它,因为没有似乎没有任何方法可以让 Ruby 不这样做。您可以通过提供一个小测试用例来演示该问题并让我知道要查找的内容而获得奖励。 很高兴为您提供帮助。我也看了一眼解析器代码。我建议你从csspool/lib/csspool/visitors/to_css.rb 中的escape_css_* 方法开始研究。有对带有代码块的 gsub 的可疑调用。如果我没记错的话,它会在 ruby​​ 堆中分配 |char| 块变量,而你的 css 文件中的背景大约是 1M 字符。 @mudasobwa 这是否意味着任何使用小变量(以及 RVALUE)的长时间运行的 ruby​​ 进程都会随着时间的推移而增长?我无法完全理解这一点。你能指出这方面的任何官方(或免费)文件吗? @YWCAHello 不,这并不意味着 任何 进程会增加它的大小。 GC 后的 RValues 返回给 Ruby 内存管理器。当且仅当没有更多空闲插槽可用时,才会从 OS 请求新内存,这意味着该进程会尝试同时创建大量 RValue(例如,通过一次处理具有不同唯一小值的外部 CSV,而不是批量处理。)在正常情况下,它会重用 GC 的插槽。 DuckDuckGo “Ruby RValue 内存管理”或查看 Ruby 源代码 (c) 了解详细信息。【参考方案2】:

好的,我找到了答案。我将留下我的另一个答案,因为该信息很难收集,它是相关的,并且可以帮助其他人搜索相关问题。

然而,您的问题似乎是由于 Ruby 实际上在获得内存后将内存释放回操作系统。

内存分配

虽然 Ruby 程序员并不经常担心内存分配,但有时会出现以下问题:

为什么在我清除了对大对象的所有引用之后,我的 Ruby 进程仍然如此庞大?我/肯定/ GC 已经运行了几次并释放了我的大对象并且我没有泄漏内存。

C 程序员可能会问同样的问题:

我free()-ed了很多内存,为什么我的进程还是这么大?

从内核分配给用户空间的内存在大块中更便宜,因此用户空间通过自己做更多的工作来避免与内核的交互。

用户空间库/运行时实现了一个内存分配器(例如:libc 中的 malloc(3)),它占用大量内核内存 2 并将它们分成更小的块供用户空间应用程序使用。

因此,在用户空间需要向内核请求更多内存之前,可能会发生多次用户空间内存分配。因此,如果您从内核中获得了一大块内存并且只使用了其中的一小部分,那么该大块内存仍然处于分配状态。

将内存释放回内核也是有代价的。用户空间内存分配器可能会(私下)保留该内存,希望它可以在同一进程中重用,而不是将其交还给内核以供其他进程使用。 (Ruby Best Practices)

因此,您的对象很可能已被垃圾收集并释放回 Ruby 的可用内存,但由于 Ruby 从不将未使用的内存返还给操作系统,因此进程的 rss 值保持不变,即使在垃圾收集之后也是如此。这实际上是设计使然。根据Mike Perham:

...而且由于 MRI 从不归还未使用的内存,我们的守护程序在仅使用 100-200 MB 时很容易占用 300-400MB。

请务必注意,这本质上是设计使然。 Ruby 的历史主要是作为文本处理的命令行工具,因此它重视快速启动和较小的内存占用。它不是为长时间运行的守护进程/服务器进程而设计的。 Java 在其客户端和服务器 VM 中进行了类似的权衡。

【讨论】:

一个简单的测试表明 Ruby 确实释放了内存(至少就 rss 值而言)。运行这个(原谅缺少空格):def report; puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#$$"`.strip.split.map(&:to_i)[1].to_s + 'KB'; end; report; big_var = " " * 10000000; report; big_var = nil; report; ObjectSpace.garbage_collect; sleep 1; report;结果是:内存7132KB 内存17100KB 内存17100KB 内存7340KB 嗯,这个结果看起来很奇怪而且不一致。当使用 1.9.3-p429 我得到以下结果:At Program Start: Memory 51408KB; After Creating big var: Memory 61176KB; After setting big var to nil: Memory 61176KB; After Garbage Collection: Memory 61176KB; 使用 Ruby 2.0.0-p195 我得到这些结果:At Program Start: Memory 80772KB; After Creating big var: Memory 90540KB; After setting big var to nil: Memory 90540KB; After Garbage Collection: Memory 90612KB; 使用 1.9.3p484 时,我得到的结果与使用 2.0.0-p247 -Memory 9364KB,Memory 19136KB,Memory 19136KB,Memory 9368KB 的运行类似。在我的生产应用程序中,当处理除触发 CSS 解析器的请求之外的任何请求时,我绝对可以看到 RSS 大小上升然后下降。也许您的第二个链接只是在谈论旧版本的 Ruby,或者指的是不同的测量值(虚拟内存大小?)。另外,mikeperham.com/2009/05/25/memory-hungry-ruby-daemons/… 你使用的是什么操作系统? Ubuntu 13.10 用于开发,CentOS 6.4 用于生产。我在两者的 big_var 测试中都看到了类似的结果——在变量被引用并且 GC 运行后 RSS 恢复。【参考方案3】:

根据@mudasobwa 的解释,我终于找到了原因。 CSSPool 中的代码正在检查非常长的数据 URI 以查找转义序列。它会使用匹配转义序列或单个字符的正则表达式在 URI 上调用 scanmap 将这些结果转义,然后将 join 重新转换为字符串。这有效地为 URI 中的每个字符分配了一个字符串。 I modified it 到 gsub 的转义序列,似乎有相同的结果(所有测试都通过)并且大大减少了使用的结束内存。

使用与最初发布的相同的测试用例(减去Mass.print 输出),这是更改前的结果:

Memory 12404KB
Memory 292516KB

这是更改后的结果:

Memory 12236KB
Memory 19584KB

【讨论】:

周年纪念答案? ;)【参考方案4】:

这可能是由于 Ruby 1.9.3 及更高版本中的“延迟扫描”功能所致。

延迟清除基本上意味着,在垃圾回收期间,Ruby 仅“清除”足够的对象以为它需要创建的新对象创建空间。它这样做是因为,当 Ruby 垃圾收集器运行时,其他任何事情都没有。这就是所谓的“停止世界”垃圾收集。

本质上,懒惰的扫描减少了 Ruby 需要“停止世界”的时间。你可以阅读更多关于懒惰扫的内容here。

你的RUBY_GC_MALLOC_LIMIT 环境变量是什么样的?

这是Sam Saffron's blog 中关于惰性扫描和 RUBY_GC_MALLOC_LIMIT 的摘录:

Ruby 2.0 中的 GC 有两种不同的风格。我们有一个“完整”的 GC,它在我们分配超过 malloc_limit 之后运行,还有一个惰性扫描(部分 GC),如果我们堆中的空闲槽用完,就会运行。

惰性扫描比完整 GC 花费的时间更少,但只执行部分 GC。它的目标是更频繁地执行一次短 GC,从而提高整体吞吐量。世界停止了,但时间更短了。

malloc_limit 设置为开箱即用的 8MB,您可以通过将 RUBY_GC_MALLOC_LIMIT 设置得更高来提高它。

您的RUBY_GC_MALLOC_LIMIT 非常高吗?我的设置为 100000000 (100MB)。默认值约为 8MB,但对于 Rails 应用程序,他们建议它更高一些。如果你的值太高,它可能会阻止 Ruby 删除垃圾对象,因为它认为它有足够的增长空间。

【讨论】:

我没有修改那个设置,虽然我想它可能是由某些东西设置的。我将如何检查它? 我已修改问题以包含“GC.stat”的输出,如您的链接中所述。 如果您更改了 RUBY_GC_MALLOC_LIMIT,您可能会知道(除非其他人为您设置了您的机器)。您可以通过在命令行中键入 echo $RUBY_GC_MALLOC_LIMIT 来查看它是否是手动设置的。如果它没有返回值,那么它可能设置为默认值 8mb。我会继续思考这个。 是的,我试过 echo $RUBY_GC_MALLOC_LIMIT 并没有得到任何结果,所以我想我可能找错地方了。此外,关于您的惰性 GC 和完整 GC,我的生产应用程序中这个问题的结果是我的服务器上的所有内存都被消耗,直到进程被杀死,所以我假设 Ruby 那时会做任何它可以做的事情更多内存。 ObjectSpace.garbage_collectbig_var = " " * 100000000; big_var = nil 之后什么都不做。如果我再添加一个0,它确实会(从 1gb 降低到 5mb)。 Ruby 2.0,MacOS。

以上是关于在 Ruby 中查找内存泄漏的原因的主要内容,如果未能解决你的问题,请参考以下文章

使用 Char* 查找内存泄漏的原因

如何防止java中的内存泄漏

重新仪器“内存泄漏”分析,其他工具是不是提供更多信息以找到泄漏的根本原因?

在 LeakCanary 泄漏跟踪中查找原因

Ruby 生产服务器内存泄漏

Android内存泄漏查找和解决