ruby内存泄漏的罪魁祸首 - 幽灵指针
程序员文章站
2022-05-01 14:05:25
...
ruby内存泄漏问题由来已久,几乎是一个无法克服的顽症。JavaEye对该问题有过探讨:Ruby VM的GC的思考。最近Ruby核心开发团队的邮件列表上面也对该问题进行了深刻的讨论,并且取得了一些相当不错的进展。
最早是有人报告了ruby的callcc调用引起的一个非常明显的内存泄漏现象:
随后邮件列表围绕该问题进行了激烈的探讨:改进的C编码技巧解决Ruby内存泄漏
Brent Roman表示上述代码的内存泄漏根源在于GCC编译器对栈的分配的特点以及Ruby传统垃圾收集器的弱点造成的。
Brent Roman在memory leak in calcc这个帖子中指出:
gcc编译器不会自动初始化那些未被使用的、未初始化赋值的变量,而这些变量一旦被那些老的,未使用的,残留在内存栈中合法指针所引用以后,垃圾收集器GC就无法收集这些未被使用的变量了。
Brent Roman在ARM芯片的设备上面用Ruby 1.6.8开发机器人程序,在他的设备上内存只有32MB,但是ruby程序一天跑下来,就会吃掉超过20MB的内存。所以Brent花了很多时间给他的ruby 1.6.8打补丁。Brent介绍说,经过他自己的hack,目前他的ruby程序吃掉的内存已经稳定在10MB以下了。
Ruby内存泄漏的罪魁祸首原来在于“幽灵指针”!当应用程序的一个新的内存栈帧被推入到内存栈顶的时候,gcc编译的程序并不是让新的栈帧简单的覆盖先前的保存在该位置的栈帧,而且还会创建出来一些空闲的栈帧。而这些新的空闲栈帧有可能会被那些老的、残留在栈中的合法指针(即幽灵指针)所引用(例如空闲栈帧的地址刚好有一个老的指针指向该地址),这样一旦Ruby的传统GC垃圾收集器访问这些指针,这些指针指向的栈帧就被激活了,这就意味着这些空闲的栈帧再也无法被垃圾收集器回收,于是内存泄漏就诞生了!
此外ruby的eval方法调用的实现也有很大的问题,他会严重导致gcc创建出来大量的体积庞大的空闲栈帧。eval方法的每次调用会导致4KB的栈地址分配,这其中只有不到20%的栈空间会被真正初始化,这意味着:
1、幽灵指针有很高的可能性引用到超过80%的空闲栈帧,从而导致内存泄漏;
2、GC必须扫描一个包含了大量空闲栈帧的内存地址空间,把很多幽灵指针指向的永远不会被用到的对象标记为可用,而无法回收;
3、callcc方法调用(用来实现Continuations编程)和Ruby多线程应用程序的线程上下文切换会导致大量无用堆栈的拷贝操作
4、ruby递归调用很容易产生堆栈溢出
Brent Roman修改了eval实现以后,测试结果堆栈空间下降了超过2/3,同时线程上下文切换速度提高了3-4倍的速度。
由于Ruby on Rails框架大量使用了eval方法调用,产生内存泄漏的现象是非常明显的。Rails2.2已经开始支持多线程运行了,但是由于幽灵指针导致的内存泄漏问题,多线程切换会是一个严重的性能瓶颈,所以用Rails多线程,还是要三思而后行为好。
最后,Ruby的创始人*也发表了看法,鼓励Brent Roman早日将他的补丁移植到ruby 1.8.7版本和ruby 1.9版本上:
所以让我们耐心的期待一段时间吧。也许困扰我们的ruby内存泄漏问题,很快就将成为历史,不复存在!
最早是有人报告了ruby的callcc调用引起的一个非常明显的内存泄漏现象:
while true @x = proc {|c| c} end # 运行正常 while true x = callcc {|c| c} end # 运行也正常 while true @x = callcc {|c| c} end # 严重泄漏内存!. while true g = Generator.new {|x| (1..3).each {|i| x.yield i}} end # 严重泄漏内存
随后邮件列表围绕该问题进行了激烈的探讨:改进的C编码技巧解决Ruby内存泄漏
Brent Roman表示上述代码的内存泄漏根源在于GCC编译器对栈的分配的特点以及Ruby传统垃圾收集器的弱点造成的。
Brent Roman在memory leak in calcc这个帖子中指出:
gcc编译器不会自动初始化那些未被使用的、未初始化赋值的变量,而这些变量一旦被那些老的,未使用的,残留在内存栈中合法指针所引用以后,垃圾收集器GC就无法收集这些未被使用的变量了。
Brent Roman在ARM芯片的设备上面用Ruby 1.6.8开发机器人程序,在他的设备上内存只有32MB,但是ruby程序一天跑下来,就会吃掉超过20MB的内存。所以Brent花了很多时间给他的ruby 1.6.8打补丁。Brent介绍说,经过他自己的hack,目前他的ruby程序吃掉的内存已经稳定在10MB以下了。
Ruby内存泄漏的罪魁祸首原来在于“幽灵指针”!当应用程序的一个新的内存栈帧被推入到内存栈顶的时候,gcc编译的程序并不是让新的栈帧简单的覆盖先前的保存在该位置的栈帧,而且还会创建出来一些空闲的栈帧。而这些新的空闲栈帧有可能会被那些老的、残留在栈中的合法指针(即幽灵指针)所引用(例如空闲栈帧的地址刚好有一个老的指针指向该地址),这样一旦Ruby的传统GC垃圾收集器访问这些指针,这些指针指向的栈帧就被激活了,这就意味着这些空闲的栈帧再也无法被垃圾收集器回收,于是内存泄漏就诞生了!
此外ruby的eval方法调用的实现也有很大的问题,他会严重导致gcc创建出来大量的体积庞大的空闲栈帧。eval方法的每次调用会导致4KB的栈地址分配,这其中只有不到20%的栈空间会被真正初始化,这意味着:
1、幽灵指针有很高的可能性引用到超过80%的空闲栈帧,从而导致内存泄漏;
2、GC必须扫描一个包含了大量空闲栈帧的内存地址空间,把很多幽灵指针指向的永远不会被用到的对象标记为可用,而无法回收;
3、callcc方法调用(用来实现Continuations编程)和Ruby多线程应用程序的线程上下文切换会导致大量无用堆栈的拷贝操作
4、ruby递归调用很容易产生堆栈溢出
Brent Roman修改了eval实现以后,测试结果堆栈空间下降了超过2/3,同时线程上下文切换速度提高了3-4倍的速度。
由于Ruby on Rails框架大量使用了eval方法调用,产生内存泄漏的现象是非常明显的。Rails2.2已经开始支持多线程运行了,但是由于幽灵指针导致的内存泄漏问题,多线程切换会是一个严重的性能瓶颈,所以用Rails多线程,还是要三思而后行为好。
最后,Ruby的创始人*也发表了看法,鼓励Brent Roman早日将他的补丁移植到ruby 1.8.7版本和ruby 1.9版本上:
matz 写道
We are troubled by the "ghost references from the machine stack" generated by GCC for years. We are more than happy to see the patch, and merge it if it's acceptable.
所以让我们耐心的期待一段时间吧。也许困扰我们的ruby内存泄漏问题,很快就将成为历史,不复存在!
上一篇: 深度学习常见问题(二)-特征工程概述
下一篇: ThreadGroup
推荐阅读