Ruby的Regexp内插会泄漏内存吗?

时间:2019-06-05 16:06:00

标签: ruby memory-leaks ruby-2.4

我在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

所以...这是一次真正的泄漏,对吧?

2 个答案:

答案 0 :(得分:1)

GC确实会杀死未使用的对象并为Ruby进程释放内存,但是Ruby进程从不将这些内存释放给OS 。但这与内存泄漏不同(因为在正常情况下,Ruby进程有时会分配足够的内存,并且不再增长-粗略地说)。当GC无法释放内存时(由于错误,错误的代码等),内存泄漏发生 ,而Ruby进程不得不借用越来越多的内存。

您的代码不是这种情况-它不包含内存泄漏,但是确实包含效率问题。

100.times { /#{STR}/i }发生的事情是你

  1. 创建100个非常长的字符串(在模式文字内插常量时)...

  2. ...,然后从这些字符串创建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

(例如,不记住字符串本身,而是记住作为常量创建的模式,然后再使用它)在testString的同一Regexp调用期间减少内存分配类(按memory_profiler的报告)。

答案 1 :(得分:1)

那是内存泄漏!

https://bugs.ruby-lang.org/issues/15916

应该在Ruby的下一版本(2.6.4或2.6.5?)中修复吗?