繁体   English   中英

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 ),然后t.test将仅使用几个KB,然后将近MB,然后再几个MB ,然后每次返回5MB, GC.start只会再次收集最后5个。

获得相同结果而不会发生内存泄漏的另一种方法是用RegExp.new(STR, true)替换/#{STR}/i 这对我来说似乎很好。

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

更新:好的,也许我读错了。 在运行GC.start ,我一直在查看GC.start容器的内存使用情况,该内存使用情况有时可能会下降,但是由于Ruby并不总是释放它没有使用的内存,我想可能只是Ruby 使用了此内存,然后,即使没有保留它,也没有将内存释放回操作系统。 使用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

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

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

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

那是内存泄漏!

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM