Ruby Ruby 的内存陷阱

wosuopu · 2015年05月14日 · 最后由 nagae_memooff 回复于 2015年08月03日 · 4874 次阅读
本帖已被设为精华帖!

Ruby有一套自动的内存管理机制。这在大多数情况下是不错的,但是有时它却是个麻烦。

Ruby的内存管理既简洁又笨重。它将对象(名为 RVALUE)存储在大约有16KB大小的堆中。 从底层上,RVALUE 是一个 C 的结构体,它包含了一个共同体表示不同的标准ruby对象。

因此在堆中存储着大小不超过40字节的 RVALUE 对象,如 StringArrayHash等。 这意味着小的对象在堆中很合适,但是一旦它们达到到阈值,那么就需要在Ruby的堆之外再分配一片额外的内存。

这块额外的内存空间是灵活的。一旦对象被垃圾回收了它就会被释放掉。但是堆本身的内存是不会被释放给操作系统的。

让我们来看一个简单的例子:

def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end

report
big_var = " " * 10_000_000
report
big_var = nil
report
ObjectSpace.garbage_collect
sleep 1
report

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

这里我们分配了大量的内存,使用完后又释放给操作系统。这一切看起来似乎没有问题。现在让我们稍微修改一下代码:

-  big_var = " " * 10_000_000
+  big_var = 1_000_000.times.map(&:to_s)

这只是一个简单的修改,不是吗。但是结果:

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

怎么回事?内存没有释放归还给操作系统。这是因为数组中的每个元素符合 RVALUE 的大小并存储在ruby的堆中。

在大多情况下这是正常的。现在ruby堆中多了许多空的位置,再次运行代码将不会再消耗额外的内存了。 每次我们处理 big_var 和一些空的堆时, GC[:heap_used]的值果然减小了。 对于这些操作Ruby是早有准备,注意这里是Ruby而不是操作系统。

因此,对于创建大量的符合40个字节的临时变量就要注意了:

big_var = " " * 10_000_000
big_var.gsub(/\s/) { |c| '-' }

结果同样是Ruby的内存疯狂增长,并且这部分内存在程序运行期间是不会归还给操作系统的:

# ⇒ Memory 10156KB
# ⇒ Memory 13788KB
# ⇒ Memory 13788KB
# ⇒ Memory 12808KB

这个问题不是太重要,稍微注意一下即可。

我的博客:http://www.xefan.com/archives/84145.html

共收到 15 条回复

为什么我运行了第一段代码,内存也没有释放?

irb(main):008:0> report
Memory 48900KB
=> nil
irb(main):009:0> big_var = nil
=> nil
irb(main):010:0> report
Memory 48900KB
=> nil
irb(main):011:0> ObjectSpace.garbage_collect
=> nil
irb(main):012:0> sleep 1
=> 1
irb(main):013:0> report
Memory 48900KB
=> nil
ruby -v
ruby 2.1.5p273 (2014-11-13 revision 48405) [x86_64-darwin14.0]

这块的内存是否归还给系统,取决于操作系统对于 malloc 的实现吧?Ruby 要背锅吗?

第一段代码

linux

Memory 7040KB
Memory 16744KB
Memory 16920KB
Memory 7164KB

osx

Memory 7720KB
Memory 17504KB
Memory 17504KB
Memory 17516KB

ruby扑街

为何OSX还没Linux的表现好啊

def report
   'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end

d1 = report
big_var = " " * 10_000_000
d2 = report
big_var = nil
d3 = report
ObjectSpace.garbage_collect
sleep 1
d4 = report

?>   d1
=> "Memory 40900KB"
>> d2
=> "Memory 40908KB"
>> d3
=> "Memory 40908KB"
>> d4
=> "Memory 40908KB"


s1 = report
big_var =  1_000_000.times.map(&:to_s)
s2 = report
big_var = nil
s3 = report
ObjectSpace.garbage_collect
sleep 1
s4 = report


>> s1
=> "Memory 10020KB"
>> s2
=> "Memory 149492KB"
>> s3
=> "Memory 149512KB"
>> s4
=> "Memory 139896KB"
>>


ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-darwin12] && OSX

效果和上面出入略大呢?怎么破?

尝试的两种情况是这样: Memory 7200KB Memory 16980KB Memory 16980KB Memory 16984KB

Memory 7072KB Memory 77060KB Memory 77060KB Memory 79388KB

看来这在不同的系统环境下执行的结果也不一样

楼主的意思是说RVALUE使用的waterMark增加会导致heap增加,但是waterMark下降,heap确不会收缩吗?个人感觉就是时间的问题,稍微等一段时间不生成大量的object应该会自动收缩吧,毕竟heap生成的成本也比较大,而且heap的使用也是很难预测的。有对Ruby实现比较熟悉的同学出来回答下?

然后又在网上查了下:Disclaimer, Ruby GC does have the code to shrink the heap. I have yet to see that happen in reality because the conditions for heap to shrink rarely happen in production applications. https://www.airpair.com/ruby-on-rails/performance

那么问题来啦,what's the condition for Ruby to shrink the heap?看来还是要看实现啊

别忘了,操作系统是很智能的。

这是我的结果,第三点和楼主说的还是比较有区别的,第三个测试我这里回收了很多内存。

victor How Ruby Uses Memory 中提及了此贴 04月17日 19:14
kingguy 记录一次排查 Puma 内存占用过高的问题 中提及了此贴 03月14日 21:38
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册