Ruby Ruby 的内存陷阱

wosuopu · 2015年05月14日 · 最后由 nagae_memooff 回复于 2015年08月03日 · 7487 次阅读
本帖已被管理员设置为精华贴

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

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

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
需要 登录 后方可回复, 如果你还没有账号请 注册新账号