Ruby 的 Regexp 插值是否泄漏内存?

Does Ruby's Regexp interpolation leak memory?

我在 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,然后差不多 1 ​​MB,然后几 MB,然后每次回到 5MB,再一次,GC.start 只会收集最后 5 个。

在没有内存泄漏的情况下获得相同结果的另一种方法是将 /#{STR}/i 替换为 RegExp.new(STR, true)。这对我来说似乎很管用。

这是 Ruby 中的合法内存泄漏还是我做错了什么?

更新: 好吧,也许我误读了这个。我在 运行 宁 GC.start 之后查看 docker 容器的内存使用情况,有时会下降,但由于 Ruby 并不总是释放它未使用的内存,我猜可能只是Ruby 使用了这块内存,然后,虽然没有被保留,但还是没有把内存释放回给OS.使用 MemoryProfiler gem 我看到 total_retained,即使在 运行 多次使用它之后也是 0。

这里的根本问题是我们有容器崩溃,理论上是由于内存使用,但也许这不是内存泄漏,而只是缺少足够的内存来允许 Ruby 消耗它想要的东西?是否有 GC 的设置可以帮助它在 Ruby 运行 内存不足和崩溃之前决定何时进行清理?

更新 2: 这仍然没有意义 - 因为为什么 Ruby 会继续从 运行 中分配越来越多的内存一遍又一遍地进行相同的过程(为什么它不使用以前分配的内存)?据我了解,GC 被设计为 运行 在从 OS 分配更多内存之前至少分配一次,所以为什么 Ruby 只是在我 运行 分配越来越多的内存] 这几次?

更新 3: 在我的独立测试中,Ruby 似乎确实接近了一个极限,无论我 运行 多少次它都会停止分配额外的内存] 测试(似乎通常在 120MB 左右),但在我的生产代码中,我还没有达到这样的限制(它在没有减速的情况下超过 500MB - 可能是因为有更多这种内存使用情况分散class 左右)。它使用的内存量可能有限制,但它似乎比 运行 这段代码所需的内存高出许多倍(它实际上只使用十几 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 "+", }' /proc/`pidof ruby`/smaps) | bc].chomp.to_i
  puts "process memory: #{mem}"
end

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

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

您的代码并非如此 - 它不包含内存泄漏,但确实包含效率问题。

当你这样做时会发生什么 100.times { /#{STR}/i } 是你

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

  2. ...然后从这些字符串创建 100 个正则表达式。

所有这些都需要不必要的分配,使 Ruby 进程使用更多内存(并且性能也会下降 - GC 非常昂贵)。将 class 定义更改为

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 和 [=14] 在同一个 test 调用期间的内存分配=] class 的数量级(根据 memory_profiler 的报告)。

内存泄漏!

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

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