我在Ruby 2.4.4上的Sinatra应用程序中有泄漏内存的代码,尽管它不是完全稳定的,但我可以在irb中重现它,我想知道其他人是否也有同样的问题。在正则表达式文字内插大字符串时会发生这种情况:
class Leak
STR = "RANDOM|STUFF|HERE|UNTIL|YOU|GET|TIRED|OF|TYPING|AND|ARE|SATISFIED|THAT|IT|WILL|LEAK|ENOUGH|MEMORY|TO|NOTICE"*100
def test
100.times { /#{STR}/i }
end
end
t = Leak.new
t.test # If I run this a few times, it will start leaking about 5MB each time
现在,如果我在此之后运行GC.start
,它通常会清理掉最后5MB(或正在使用的内存),然后t.test
将只使用几个KB,然后几乎一个MB,然后是几个MB,然后每次又回到5MB,再一次,GC.start
将只收集最后5个。
获得相同结果而不发生内存泄漏的另一种方法是将/#{STR}/i
替换为RegExp.new(STR, true)
。这对我来说似乎很好。
这是Ruby中的合法内存泄漏,还是我做错了什么?
更新:
好吧,也许我读错了。运行GC.start
之后,我一直在查看docker容器的内存使用情况,有时这种情况可能会下降,但是由于Ruby并不总是释放它没有使用的内存,我想可能只是Ruby uses 该内存,然后,即使不保留它,也仍然没有将内存释放回操作系统。使用MemoryProfiler gem,即使经过多次运行,total_retained也为0。
这里的根本问题是,从理论上讲,由于内存使用,我们使容器崩溃了,但也许这不是内存泄漏,而是缺少足够的内存来允许Ruby消耗其想要的东西? GC是否有设置可帮助其确定何时应该在Ruby耗尽内存并崩溃之前进行清理?
更新2:但这仍然没有道理-因为Ruby为什么会一遍又一遍地运行同一进程来继续分配越来越多的内存(为什么它不使用内存?以前分配的)?据我了解,GC被设计为在从OS分配更多内存之前至少运行一次,那么为什么当我多次运行Ruby时,Ruby只会分配越来越多的内存?
更新3:在我的隔离测试中,Ruby确实达到了一个极限,即无论我运行测试多少次(似乎通常为120MB左右),它都会停止分配额外的内存,但是在我的生产代码中,我还没有达到这样的限制(超过500MB却没有减慢速度-可能是因为在类周围散布了更多此类内存使用情况的实例)。可能使用多少内存是有限制的,但似乎比运行此代码所需的内存高出很多(实际上一次运行只使用十几个MB)
更新4:我将测试用例的范围缩小到了真正泄漏的地方!从文件中读取多字节字符是重现实际问题的关键:
str = "String that doesn't fit into a single RVALUE, with a multibyte char:" + 160.chr(Encoding::UTF_8)
File.write('weirdstring.txt', str)
class Leak
PATTERN = File.read("weirdstring.txt").freeze
def test
10000.times { /#{PATTERN}/i }
end
end
t = Leak.new
loop do
print "Running... "
t.test
# If this doesn't work on your system, just comment these lines out and watch the memory usage of the process with top or something
mem = %x[echo 0 $(awk '/Private/ {print "+", $2}' /proc/`pidof ruby`/smaps) | bc].chomp.to_i
puts "process memory: #{mem}"
end
所以...这是一次真正的泄漏,对吧?
答案 0 :(得分:1)
GC确实会杀死未使用的对象并为Ruby进程释放内存,但是Ruby进程从不将这些内存释放给OS 。但这与内存泄漏不同(因为在正常情况下,Ruby进程有时会分配足够的内存,并且不再增长-粗略地说)。当GC无法释放内存时(由于错误,错误的代码等),内存泄漏发生 ,而Ruby进程不得不借用越来越多的内存。
您的代码不是这种情况-它不包含内存泄漏,但是确实包含效率问题。
100.times { /#{STR}/i }
发生的事情是你
创建100个非常长的字符串(在模式文字内插常量时)...
...,然后从这些字符串创建100个正则表达式。
所有这些都需要不必要的分配,这会使Ruby进程使用更多的内存(并且也会降低性能-GC相当昂贵)。将类定义更改为
class Leak
PAT = /"RANDOM|STUFF|HERE|UNTIL|YOU|GET|TIRED|OF|TYPING|AND|ARE|SATISFIED|THAT|IT|WILL|LEAK|ENOUGH|MEMORY|TO|NOTICE"*100/i
def test
100.times { PAT }
end
end
(例如,不记住字符串本身,而是记住作为常量创建的模式,然后再使用它)在test
和String
的同一Regexp
调用期间减少内存分配类(按memory_profiler
的报告)。
答案 1 :(得分:1)