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

时间:2013-12-04 20:57:17

标签: ruby memory-leaks valgrind

我在Rails代码中发现了内存泄漏 - 也就是说,我发现代码泄漏了什么,但为什么泄漏了。我把它简化为一个不需要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
据说{p> 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相关的东西,只有宝石信息等等。

我也尝试输出“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

结果为here。我不确定这是否证实了C级泄漏,因为我还读到Ruby使用Valgrind不理解的内存做事。

使用的版本:

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

4 个答案:

答案 0 :(得分:37)

看起来你正在进入 The Lost World 。我认为问题不在于racc中的c-bindings。

Ruby内存管理既优雅又繁琐。它将对象(名为RVALUE s)存储在大小约为16KB的所谓中。在较低级别,RVALUE是一个c-struct,包含union个不同的标准ruby对象表示。

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

这种额外的记忆力是灵活的;一旦对象变成GC,它就会被释放。这就是为什么带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大小,存储在中的红宝石堆中

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

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

希望它有所帮助。

答案 1 :(得分:14)

好的,我找到了答案。我正在离开我的另一个答案,因为这些信息很难收集,它是相关的,它可以帮助其他人搜索相关问题。

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

  

内存分配

     

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

     

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

     

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

     

我自由() - 记忆很多,为什么我的过程仍然那么大?

     

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

     

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

     

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

     

将内存释放回内核也需要付出代价。用户空间内存分配器可以(私下)保留在该内存上,希望它可以在同一进程中重用,而不是将其返回给内核以便在其他进程中使用。 (Ruby Best Practices)

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

  

...由于MRI永远不会回放未使用的内存,因此我们的守护进程只需使用100-200就可以轻松获取300-400MB。

     

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

答案 2 :(得分:8)

这可能是由于Ruby 1.9.3及更高版本中的“Lazy Sweeping”功能。

懒惰扫描基本上意味着,在垃圾收集期间,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是否非常高?我的设置为1亿(100MB)。默认值大约为8MB,但对于rails应用程序,他们建议它要高一些。如果你的太高,可能会阻止Ruby删除垃圾对象,因为它认为它有足够的增长空间。

答案 3 :(得分:8)

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

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

Memory 12404KB
Memory 292516KB

这是改变后的结果:

Memory 12236KB
Memory 19584KB