我在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不理解的内存做事。
使用的版本:
答案 0 :(得分:37)
看起来你正在进入 The Lost World 。我认为问题不在于racc
中的c-bindings。
Ruby内存管理既优雅又繁琐。它将对象(名为RVALUE
s)存储在大小约为16KB的所谓堆中。在较低级别,RVALUE
是一个c-struct,包含union
个不同的标准ruby对象表示。
因此,堆存储RVALUE
个对象,其大小不超过40个字节。对于String
,Array
,Hash
等对象,这意味着小对象可以放入堆中,但一旦达到阈值,就会在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上的scan
,map
将这些结果转换为unescape,然后将join
转换回字符串。这实际上是为URI中的每个字符分配一个字符串。 I modified it到gsub
转义序列,它们似乎具有相同的结果(所有测试都通过)并大大减少了使用的结束内存。
使用与最初发布的相同的测试用例(减去Mass.print
输出),这是更改前的结果:
Memory 12404KB
Memory 292516KB
这是改变后的结果:
Memory 12236KB
Memory 19584KB