我在 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使用了这个内存,然后,即使它没有被保留,它仍然没有将内存释放回操作系统。 使用内存探查器gem,我看到total_retained,即使运行了几次也是0。
这里的根本问题是我们的容器崩溃了,理论上是由于内存使用,但也许这不是内存泄漏,而只是缺乏足够的内存来允许 Ruby 消耗它想要的东西? GC 是否有设置来帮助它在 Ruby 内存不足和崩溃之前决定何时进行清理?
更新 2:这仍然没有意义 - 因为为什么 Ruby 会因为一遍又一遍地运行相同的进程而继续分配越来越多的内存(为什么它不使用以前分配的内存)? 据我了解,GC 被设计为在从操作系统分配更多内存之前至少运行一次,那么为什么 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
所以......这是一个真正的泄漏,对吧?
这是内存泄漏!
https://bugs.ruby-lang.org/issues/15916
应该在 Ruby 的下一个版本(2.6.4 或 2.6.5?
GC 确实会杀死未使用的对象并为 Ruby 进程释放内存,但 Ruby 进程永远不会将此内存释放给操作系统。但这与内存泄漏不同(因为在正常情况下,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
(例如,不记住字符串本身,而是记住从它创建的模式作为常量,然后重用它)减少了String
和Regexp
类在同一test
调用期间的内存分配(根据memory_profiler
的报告)。