Memory leak profiling with Ruby: ObjectSpace

February 24, 2008 – 11:59 pm

ruby内存泄漏?!不是有 Garbage Collector 吗?为什么在带 GC 的语言里还会有内存泄漏?GC 难道不是来帮助我们摆脱万恶的内存分配问题的吗?确实如此,但是 GC 也不是万能的。垃圾收集器并不知道你需要用到哪些对象,所以它假定能被你(通过某个变量直接或间接)引用到的对象是你会用到的,而其他的则是垃圾——这是相当保守的做法,但是至少是正确的。但是这样带来的一个问题就是:如果你(有意或者无意地)持有了一些(你以后根本不会用到的)对象的引用(例如,通过一个全局变量),就造成了内存泄漏。

看起来在带 GC 的语言里的内存泄漏比普通的 C 语言类的程序里的内存泄漏问题更难处理。在 Ruby 里也有同样的问题, _why 也有一篇文章讲了 Ruby 的内存泄漏问题。

RMMSeg 目前的版本(0.1.1)里也有严重的内存泄漏问题。事实上,robbin 在试用了 RMMSeg 之后对性能非常失望,还提出了内存泄漏的问题。确实是我在做了 RMMSeg 之后并没有用大量的数据进行过测试,一句话就是:玩具和产品还是有相当大的差别的。

不过我也希望对 RMMSeg 进行改进让他可以真正用在实际项目中。首先要解决的就是内存泄漏的问题。事实上,我对于 GC 语言里面的内存泄漏也是一片茫然,我知道引用用不到的对象会造成内存泄漏,可是有什么办法能找出在哪里泄漏了呢?如果找不到,就没办法改进了。好在 Ruby 也是一个非常灵活的语言,直接深入 ObjectSpace 做一些统计和分析也是很简单的事情。

在搜索了一番之后,我找到了这篇文章里那段最初由 Ryan Davis 所作的代码。那段代码定时分析所有的对象,并打印出数量变化最大的前 20 项。

我的第一个目的是确定在每一次 segment 之间有没有内存泄漏。为此,我对上面的代码做了一点改进,把定时触发改成了每一轮之后手动触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MemoryProfiler
  SEPARATOR = "-="*20 + "-"
 
  def initialize(dump_string=false)
    @pass = 0
    @dump_string = dump_string
    @prev = Hash.new(0)
    @file = File.open('log/memory_profiler.log', 'w')
  end
 
  def report
    GC.start
    curr = Hash.new(0)
    strings = Array.new
 
    ObjectSpace.each_object do |o|
      curr[o.class] += 1
      if @dump_string and o.is_a? String
        strings << o
      end
    end
 
    if @dump_string
      File.open("log/memory_profiler.strings.pass#{@pass}.log", "w") do |f|
        strings.sort.each do |s|
          f.puts s
        end
      end
    end
 
    delta = Hash.new(0)
    curr.keys.each do |k|
      delta[k] = curr[k] - @prev[k]
    end
 
    @file.puts SEPARATOR
    @file.puts "Pass #{@pass}:"
    delta.sort_by { |k, v| -v }[0..19].each do |k, v|
      @file.printf "%+8d: %s (%d)\n", v, k.name, curr[k] unless v == 0
    end
 
    @prev = curr
    @pass += 1
 
    GC.start
  end
end

这样,我在每进行一次分词就触发一次报告,看两次分词之间打差异就可以看出在每轮分词之后是否有内存被泄露掉了:

  File.open(input) do |f|
    cont = f.read
 
    mem_profiler = MemoryProfiler.new
    puts Benchmark.measure {
      n.times {
        mem_profiler.report
        segment(cont)
      }
      mem_profiler.report       # final report
    }
  end

下面是结果:

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 0:
  +10306: String (10306)
    +607: Array (607)
    +257: Class (257)
    +125: Regexp (125)
     +60: Proc (60)
     +60: Gem::Version (60)
     +54: Gem::Requirement (54)
     +35: Module (35)
     +28: Hash (28)
     +21: Time (21)
     +18: Gem::Specification (18)
     +17: Float (17)
     +16: Gem::Dependency (16)
      +6: File (6)
      +6: Rake::FileList (6)
      +5: Mutex (5)
      +4: Rake::Task (4)
      +3: Object (3)
      +3: Rake::DefaultLoader (3)
      +3: Bignum (3)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 1:
 +149821: String (160127)
   +1009: RMMSeg::Word (1009)
     +44: Array (651)
      +3: Hash (31)
      +1: RMMSeg::LSDMFOCWRule (1)
      +1: RMMSeg::SVWLRule (1)
      +1: RMMSeg::Dictionary (1)
      +1: RMMSeg::ComplexAlgorithm (1)
      +1: RMMSeg::MMRule (1)
      +1: RMMSeg::LAWLRule (1)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 2:
      +8: Array (659)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 3:
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 4:
……中间都是一样的……
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Pass 50:

从结果来看,在每一轮分词之间是没有内存泄漏的,那么内存的飙升就集中到单轮的分词过程上了。虽然问题还有待解决,但是至少范围缩小了,还是值得庆贺! :D

而且,利用 ObjectSpace 来查找内存泄漏还是挺方便的,而且我觉得在自己的代码的关键点处插入手工触发的代码应该比定时自动触发要容易找到问题所在。当然,如果代码量特别大而且结构复杂的话,也是没有办法的事情了。 :)

  1. 9 Responses to “Memory leak profiling with Ruby: ObjectSpace”

  2. 原以为内存泄漏比较容易解决,现在才发现原来如此……
    由于现在的计算机的配置比较高,内存泄漏了在相当长的一段时间内不会被感觉到,于是坚持“够用就好”的用户也不会抱怨这些程序。比如,金山词霸好像就存在内存泄漏问题,似乎每次屏幕取词之后,没有释放取词窗口的内存。

    By quark on Feb 25, 2008

  3. 对于一般的桌面程序,内存泄漏不是太大的问题,大不了用一段时间程序重启一次。但是对于服务端持续运行的程序而言这是致命的。

    By Goncha on Feb 25, 2008

  4. 监测ruby内存的工具,还可以试试Bleak_house:

    http://blog.evanweaver.com/files/doc/fauna/bleak_house/files/README.html

    Bleak_house给Ruby解析器打了补丁,插入相关的指令,可以从底层探测整个ruby内存堆中对象的情况,然后你可以定期dump出来完整的内存堆里面的所有对象,再用bleak工具去分析dump文件。

    By robbin on Feb 25, 2008

  5. @Goncha:
    是啊,内存再多也还是有限的, GC 也不是万金油。

    By pluskid on Feb 25, 2008

  6. @robbin:
    多谢,我去下来看看。 :)

    By pluskid on Feb 25, 2008

  7. 因为做大数据量的索引对字符串处理的运算量特别大,所以我有点担心ruby的性能是否能够承担,所以我也蛮关注RMMSeg项目的。做这种性能敏感,数据量大的项目的确是一个挑战,呵呵,对编码质量和水平的要求高很多。

    我粗略的看了一下RMMSeg的源代码,我觉得代码写的很漂亮,但是考虑到性能问题,可能很多地方需要另外做特别的优化,不过慢慢来吧,啥好东西都是打磨出来的。

    By robbin on Feb 25, 2008

  8. @robbin:
    很奇怪,从 ObjectSpace 来看,每一轮之后并没有什么对象被泄露出去,但是从外部看程序占用的内存确实一直都在上升的:

    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 1664(+1664)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853  3028   4476  7.0  0.5 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152996(+151332)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 30780  32196 70.6  5.9 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152999(+3)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 31252  32724 84.2  6.0 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152999(+0)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 32008  33384 94.0  6.1 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152999(+0)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 32632  34044 85.2  6.3 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152999(+0)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 33248  34704 90.7  6.4 ruby memory_leak.rb
     
    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    string counts: 152999(+0)
      PID  PPID   RSS    VSZ %CPU %MEM CMD
    19410 18853 33864  35232 94.6  6.5 ruby memory_leak.rb
     
    ……省略

    RSS 的数值大概如下:

    32008, 32632, 33248, 33864, 34480, 35140, 35796, 36452, 37108, 37768, 38424, 39076, 39732, 40388, 41040, 41696, 42356, 43020

    每轮的增长为:

    624, 616, 616, 616, 660, 656, 656, 656, 660, 656, 652, 656, 656, 652, 656, 660, 664

    而 VSZ 则更有规律一些,几乎每次增长都是 660 。用 BleakHouse 分析,在 segment 处做 snapshot ,分析出来说有 String 泄漏,可是没有在 ObjectSpace 里应当就是引用不到的对象,怎么泄漏了呢? BleakHouse 还不太会用,它的文档似乎很简略,我把 snapshot 设的过小了,在 next_token 那里监测,结果它运行了大半天都不出结果,产生了一个 2GB 的输出文件直接被我的 shell 给自动 kill 掉了。 -.-bb

    By pluskid on Feb 25, 2008

  9. 说一个我碰上的事情,用ruby写一个网页爬虫,看着挺稳定的,不过总不能长久运行,总是跑上一个晚上就死了。很快就知道是内存用过了头,然后写了几个测试,怎么也不能重现问题。郁闷了三两天,结果很偶然的机会,才发现,是爬虫爬到一个mms源上面,服务稳定,不停的吐啊吐啊吐,程序厚道,不停的读啊读啊读啊读,结果撑死……

    By is on Jul 13, 2008

  10. @is,
    哈哈!通用的爬虫还是不好写啊。 :D

    By pluskid on Jul 13, 2008

Post a Comment