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

Jas*_*abe 57 ruby valgrind memory-leaks

我发现我的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
Run Code Online (Sandbox Code Playgroud)

据说红宝石质量可以让我看到记忆中的所有物体.CSSPool是一个基于racc的CSS解析器./home/jason/big.css是一个1.5MB的CSS文件.

这输出:

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
==================================================
Run Code Online (Sandbox Code Playgroud)

你可以看到内存去的方式了.一些计数器上升,但没有特定于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
Run Code Online (Sandbox Code Playgroud)

但同样,这并没有给我任何与CSSPool相关的信息,只有宝石信息等等.

我也尝试输出"GC.stat"......

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

结果:

{: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}
Run Code Online (Sandbox Code Playgroud)

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

我还读到了关于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
Run Code Online (Sandbox Code Playgroud)

结果在这里.我不确定这是否证实了C级漏洞,因为我还读到Ruby用Valgrind不理解的内存做事情.

使用的版本:

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

Ale*_*kin 39

看起来你正在这里进入失落的世界.我认为问题不在于c-bindings racc.

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

因此,堆存储RVALUE对象,其大小不超过40个字节.对于这样的对象作为String,Array,Hash等等,这意味着小物体可以容纳在堆中,但一旦它们达到阈值时,红宝石堆之外的额外的存储器将被分配.

这种额外的记忆是灵活的; 一旦对象变成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
Run Code Online (Sandbox Code Playgroud)

但是,一旦被收购,堆(看GC[:heap_length])自己不会被释放回操作系统.看,我会对你的测试用例做一个单调的变化:

- big_var = " " * 10000000
+ big_var = 1_000_000.times.map(&:to_s)
Run Code Online (Sandbox Code Playgroud)

瞧,瞧:

# ? Memory 11788KB
# ? Memory 65188KB
# ? Memory 65188KB
# ? Memory 57448KB
Run Code Online (Sandbox Code Playgroud)

存储器不被释放回到OS了,因为该阵列的每个元素我介绍适合RVALUE大小和被存储在红宝石堆.

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

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

希望能帮助到你.


Joe*_*gar 15

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

你的问题,但是,似乎是由于这样的事实,红宝石实际上并没有,一旦它已经收购了它释放内存返回到操作系统.

内存分配

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

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

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

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

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

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

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

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

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

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

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


Joe*_*gar 9

这可能是由于Ruby 1.9.3及更高版本中的"Lazy Sweeping"功能.

懒惰扫描基本上意味着,在垃圾收集期间,Ruby只会"扫描"掉足够多的对象,以便为需要创建的新对象创建空间.这样做是因为,当Ruby垃圾收集器运行时,没有别的办法.这被称为"停止世界"垃圾收集.

从本质上讲,懒惰扫描减少了Ruby需要"阻止世界"的时间.你可以在这里阅读更多关于懒惰扫地的信息.

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

以下是Sam Saffron关于懒惰席卷的博客和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删除垃圾对象,因为它认为它有足够的增长空间.


Jas*_*abe 9

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

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

Memory 12404KB
Memory 292516KB
Run Code Online (Sandbox Code Playgroud)

这是改变后的结果:

Memory 12236KB
Memory 19584KB
Run Code Online (Sandbox Code Playgroud)

  • 周年纪念回答?;) (10认同)