内存泄漏?!不是有 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 之间有没有内存泄漏。为此,我对上面的代码做了一点改进,把定时触发改成了每一轮之后手动触发:
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 来查找内存泄漏还是挺方便的,而且我觉得在自己的代码的关键点处插入手工触发的代码应该比定时自动触发要容易找到问题所在。当然,如果代码量特别大而且结构复杂的话,也是没有办法的事情了。 :)
Comments (9)
由于现在的计算机的配置比较高,内存泄漏了在相当长的一段时间内不会被感觉到,于是坚持“够用就好”的用户也不会抱怨这些程序。比如,金山词霸好像就存在内存泄漏问题,似乎每次屏幕取词之后,没有释放取词窗口的内存。
监测ruby内存的工具,还可以试试Bleak_house:
http://blog.evanweaver.com/files/doc/fauna/bleak_house/files/README.html
Bleak_house给Ruby解析器打了补丁,插入相关的指令,可以从底层探测整个ruby内存堆中对象的情况,然后你可以定期dump出来完整的内存堆里面的所有对象,再用bleak工具去分析dump文件。
是啊,内存再多也还是有限的, GC 也不是万金油。
多谢,我去下来看看。 :)
因为做大数据量的索引对字符串处理的运算量特别大,所以我有点担心ruby的性能是否能够承担,所以我也蛮关注RMMSeg项目的。做这种性能敏感,数据量大的项目的确是一个挑战,呵呵,对编码质量和水平的要求高很多。
我粗略的看了一下RMMSeg的源代码,我觉得代码写的很漂亮,但是考虑到性能问题,可能很多地方需要另外做特别的优化,不过慢慢来吧,啥好东西都是打磨出来的。
@robbin: 很奇怪,从 ObjectSpace 来看,每一轮之后并没有什么对象被泄露出去,但是从外部看程序占用的内存确实一直都在上升的:
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
哈哈!通用的爬虫还是不好写啊。 :D