我最近遇到了在Tomcat的JRuby上运行Sinatra的permgen内存泄漏。问题与Sinatra用来支持各种模板选项的Tilt库有关。旧代码(此处未包含)正在生成内存泄漏。新代码(下面)没有,事实上我发现permgen GC现在正在运行。
Ruby应该是自我描述,但我无法通过阅读来弄清楚这些代码。有嵌套的类逃逸。为什么?为什么要定义一个方法然后解除绑定?
为什么代码会编译一堆模板并保留它们以便重复使用这么复杂的外观?
此外:如果有任何GitHub员工正在查看此问题,您能否向GitHub添加一些功能,允许用户在代码段中插入问题?
(此代码取自https://github.com/rtomayko/tilt/blob/master/lib/tilt.rb)
def compile_template_method(locals)
source, offset = precompiled(locals)
offset += 5
method_name = "__tilt_#{Thread.current.object_id.abs}"
Object.class_eval <<-RUBY, eval_file, line - offset
#{extract_magic_comment source}
TOPOBJECT.class_eval do
def #{method_name}(locals)
Thread.current[:tilt_vars] = [self, locals]
class << self
this, locals = Thread.current[:tilt_vars]
this.instance_eval do
#{source}
end
end
end
end
RUBY
unbind_compiled_method(method_name)
end
答案 0 :(得分:5)
有嵌套的类逃逸。为什么?
这种方法看起来像是来自战争伤痕累累,修复和修补的生产代码,而不是优雅的自描述代码(所以也许我们可以原谅他们一点)。
那为什么两个人会躲避?在第二个嵌套的“真实”模板方法代码可以被评估之前,要评估的代码必须以正确的源编码为前缀,该编码可能已被定义为模板文件中的“魔术注释”。 / p>
一旦正确设置了字符串编码,就可以尝试真正的class_eval。另一种说法可能是“这是编写源代码的源代码的源代码”!
据推测,这是为了修复Ruby 1.9中可能出现的兼容性问题,其中正在编译的模板可能包含与Tilt库源代码本身的编码不同的字符编码(UTF-8)(US-ASCII编码) ),这将导致模板字符串的错误评估(因为字符串编码已经在调用模板文件的主机代码中设置)。
为什么要定义方法然后解除绑定?
澄清:在Ruby中,未绑定与 undefined 不同。
未绑定方法作为可以调用的 UnboundMethod 类型的自由方法对象存在,尽管它们不再与特定对象关联。未绑定的方法不再具有接收方。
为了创建一个未绑定的方法,它首先被绑定(定义为)一个对象。这就是为什么编译模板方法可以从顶级对象中快速删除的原因,因为它只是生成未绑定方法所必需的临时安排。
使用此技术可以使用针对给定类的不同实例定义的编译模板,而无需以任何可见或永久的方式更改根对象或第三方开发人员的客户端类。
通过将已编译的模板方法与特定的客户端代码对象取消关联,在以后调用使用该类型对象的模板时,编译后的模板方法可以稍后反弹到该对象类的新实例。
例如,给定以下ERB模板:
<p>Hello <%= @name %></p>
...以及以下调用代码:
scott = Person.new
scott.name = "Scott"
output = template.render(scott)
=> "<p>Hello Scott</p>"
在第一次渲染期间,模板被eval'd并针对TOPOBJECT对象的实例进行编译。编译后的模板方法将命名为“__tilt_2151955260
”。然后,此方法将被解除绑定,以便针对TOPOBJECT类型的所有实例(根据Ruby版本只是Object或BasicObject)再次使用,因此可以用于任何客户端对象类型。
下次渲染模板时,编译的模板方法与TOPOBJECT的'baq'实例绑定:
baq = Person.new
baq.name = "Baq"
output = template.render(baq)
在幕后,当调用template.render(baq)
时,未绑定的编译模板方法将绑定到Person的'baq'实例:
__tilt_2151955260.bind(baq).call
每次都不必调用class_eval会带来相当大的性能提升。
为什么代码会编译一堆模板并保留它们 重复使用如此复杂的外观?
我的评估是,尽管代码实现确实看起来确实看起来不必要地复杂,但是这些间接层通常是框架代码中必需的,旨在使公共API非常简单和甜蜜地为成千上万的其他开发人员消费,即使它是以少数开发人员为代价来维护它的。
由于许多不同语言环境中使用的API引起的实际问题以及来自世界各地的许多编码,代码复杂性(双重评估嵌套)也增加了。
<强>脚注强>: 问题中提到的Template类已被重构为单独的文件github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb
答案 1 :(得分:1)
以下是我在此代码中理解的内容:
Object.class_eval将执行当前作用域之外的第一个块以及全局作用域(如果发生错误,eval_file和line-offset就是打印右行和文件名)然后将创建一个新方法一个虚拟容器(我想这是TOPOBJECT的东西),一旦编译它的方法没有绑定并存储在其他地方。
之后,该方法将附加到包含模板变量的新对象并在那里运行,我不记得确切的语法,但这里是想法(其中method是一个未绑定的方法):
object = SomeClass.new
object.param1 = "something"
object.param2 = 43
method.apply(object)
至于代码的复杂性,我已经不得不写这样的东西(并不像上面说的那么复杂),使api上面简单易用,这就是价格有时候^^