请在Clojure 2024调查问卷中分享您的想法!

欢迎!请查看关于页面以了解更多关于这个工作原理的信息。

0
协议
我们发现了一种PermGen内存泄漏,它是由在{{eval}}中调用的协议方法和多方法引起的,因为这些方法使用了缓存。只有在缓存的值是类(例如函数或reify)的实例时,即这些类是在{{eval}}内部定义的情况下,才会出现这个问题。因此,扩展{{IFn}}或在对{{IFn}}进行多方法分派可能是触发器。

**测试方法:** 我发现设置"{{-XX:MaxPermSize}}"到一个合理的值是测试这种泄露的简单方法,这样你就不必等待过长的时间来填满PermGen空间,并且使用"{{-XX:+TraceClassLoading}}"和"{{-XX:+TraceClassUnloading}}"来查看正在加载和解离的类。

{code:title=leiningen project.clj}
(defproject permgen-scratch "0.1.0-SNAPSHOT")
  :dependencies [[org.clojure/clojure "1.5.0-RC1"]]
  :jvm-opts ["-XX:MaxPermSize=32M"
-  "-XX:+TraceClassLoading"
-  "-XX:+TraceClassUnloading"])


您可以使用{{lein swank 45678}}并将slime连接到emacs中的{{M-x slime-connect}}。

要监视PermGen的使用,您可以使用"{{jps -lmvV}}"找到要监视的Java进程,然后运行"{{jstat -gcold +_<进程ID>_+ 1s}}"。根据[jstat文档|http://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html#gcold_option],第一列(PC)是“当前永久空间容量(KB)”,第二列(PU)是“永久空间利用率(KB)”。另外,VisualVM也是一个非常好的监控工具。

## 多方法泄漏

评估以下代码将运行一个循环执行的{{(take* (fn foo []))}}。

{code:title=multimethod leak}
(defmulti take* (fn [a] (type a)))

(defmethod take* clojure.lang.Fn
  [a]
  '())

(def stop (atom false))
(def sleep-duration (atom 1000))

(defn run-loop []
  (when-not @stop
    (eval '(take* (fn foo [])))
    (Thread/sleep @sleep-duration))
    (recur)))

(future (run-loop))

(reset! sleep-duration 0)


在{{lein swank}}会话中,您将看到许多如下所示的行,显示正在创建和加载的类。


[Loaded user$eval15802$foo__15803 from __JVM_DefineClass__]
[Loaded user$eval15802 from __JVM_DefineClass__]


这些行将在PermGen空间填满时停止。

在jstat监控中,您将看到使用的PermGen空间量(PU)达到最大值并保持在那里。


-    PC       PU        OC          OU       YGC    FGC    FGCT     GCT
 31616.0  31552.7    365952.0         0.0      4     0    0.000    0.129
 32000.0  31914.0    365952.0         0.0      4     0    0.000    0.129
 32768.0  32635.5    365952.0         0.0      4     0    0.000    0.129
 32768.0  32767.6    365952.0      1872.0      5     1    0.000    0.177
 32768.0  32108.2    291008.0     23681.8      6     2    0.827    1.006
 32768.0  32470.4    291008.0     23681.8      6     2    0.827    1.006
 32768.0  32767.2    698880.0     24013.8      8     4    1.073    1.258
 32768.0  32767.2    698880.0     24013.8      8     4    1.073    1.258
 32768.0  32767.2    698880.0     24013.8      8     4    1.073    1.258


一种解决办法是在PermGen空间完全用尽之前运行{{prefer-method}},例如:

(prefer-method take* clojure.lang.Fn java.lang.Object)


当使用的PermGen空间接近最大值时,在{{lein swank}}会话中,您将看到由eval创建的类被卸载。

[卸载类user$eval5950$foo__5951]
[卸载类user$eval3814]
[卸载类user$eval2902$foo__2903]
[卸载类user$eval13414]


在jstat监控中,当使用的PermGen空间接近最大值时,将出现长时间的中断,然后它会下降,并在再次发生eval'时开始增加。


-    PC       PU        OC          OU       YGC    FGC    FGCT     GCT
 32768.0  32767.9    159680.0     24573.4      6     2    0.167    0.391
 32768.0  32767.9    159680.0     24573.4      6     2    0.167    0.391
 32768.0  17891.3    283776.0     17243.9      6     2   50.589   50.813
 32768.0  18254.2    283776.0     17243.9      6     2   50.589   50.813


{{defmulti}}定义了一个使用分配值作为键的缓存。在循环中对每个eval调用定义一个新的foo类,并在call{{take*}}时将其添加到缓存中,从而防止类被GC。

{{prefer-method}}的解决办法之所以有效,是因为它调用了{{clojure.lang.MultiFn.preferMethod}},该函数调用私有的{{MultiFn.resetCache}}方法,该方法完全清空了缓存。

2. 协议泄漏

与协议方法相关的泄漏同样涉及一个缓存。如果您使用协议运行以下代码,将会看到与多方法泄漏基本上相同的行为。

{code:title=协议泄漏}
(defprotocol ITake (take* [a]))

(extend-type clojure.lang.Fn
  ITake
  (take* [this] '()))

(def stop (atom false))
(def sleep-duration (atom 1000))

(defn run-loop []
  (when-not @stop
    (eval '(take* (fn foo [])))
    (Thread/sleep @sleep-duration))
    (recur)))

(future (run-loop))

(reset! sleep-duration 0)


同样,缓存位于{{take*}}方法本身中,使用每个新的{{foo}}类作为键。

解决办法:* 在PermGen空间用完之前运行{{-reset-methods}},例如:

(-reset-methods ITake)


这有效,因为{{-reset-methods}}用空的MethodImplCache替换了缓存。

补丁:* protocol_multifn_weak_ref_cache.diff

筛选人员:*

23 答案

0

评论者: [email protected]

我认为最明显的解决方案是对缓存的尺寸进行限制。将项添加到缓存本身就不是最快的路径,因此可以再做一些工作来防止缓存无限扩大。

这引发了关于采用何种标准的疑问。保留最初的n条记录?保留n条最近使用过的记录(这需要在快速缓存命中路径中进行会计)?还是保留n条最近添加的记录?

0

评论者:jsmorph

至少,或许可以切换到禁用缓存,但要明确这种设置会对性能产生影响。

看起来昂贵的LRU逻辑可能是正确的途径,但也许不应该在达到某个阈值之前完全启用。

0

评论者:alexmiller

来自邮件列表的生产环境报告看到这种情况
https://groups.google.com/forum/#!topic/clojure/_n3HipchjCc

0

评论者:adrianm

因此,我们遇到过PermGen空间异常,这是一个相当关键的bug——我们正在代码库中广泛使用多方法,这个异常会在运行时随机出现。

0

评论者:hiredman

将这个问题分成两个可能更好,因为在非常抽象的层面上,这两个问题是“相同”的,但在具体上则是不同的(协议并不真正与多方法共享代码路径),将它们保留在一个问题中可能会是一个大而难以阅读的补丁的“原料”。

0

评论者:hiredman

将multimethods中的methodCache替换成在PersistentHashMap和PersistentQueue上构建的非常简单LRU缓存

0

评论者:hiredman

naive-lru-for-multimethods-and-protocols.diff 创建了一个新的类 clojure.lang.LRUCache,它通过IPMap接口提供使用PHashMap和PQueue构建的LRU缓存。

更改 MultiFn 以使用 LRUCache 作为其方法缓存。

更改 expand-method-impl-cache 以使用 LRUCache 作为 MethodImplCache 的映射情况。

0

评论者:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 可能是错误的,除非 MethodImplCache 确实被用作缓存,我们无法简单地在它满了之后清除条目。

再次查看 dectype 代码,看起来 MethidImplCache 确实被用作缓存,所以这个补丁可能没问题。

如果我对任何事情有把握,那就是我不确定,所以希望有把握的人能加入讨论。

0

由:bronsa 评论

我没有查看您的补丁,但可以确认,协议函数中的 MethodImplCache 仅作为缓存使用。

0

由:killme2008 评论

我开发了一个新的补丁,将 MultiFn 中的 methodCache 转换为使用 WeakReference 作为分发值,并在需要时清除缓存。

我已经与票号中的代码进行了测试,看起来没问题。类将在 perm gen 几乎用尽时卸载。

0

评论者:alexmiller

我不知道在这里要评估哪个。multifn_weak_method_cache.diff 是否取代了 naive-lru-for-multimethods-and-protocols.diff,或者这些替代方法都在考虑之中?

0

评论者:hiredman

我认为最直接的方法是将它们视为替代方案,我不是特别喜欢弱引用,但当然不使用弱引用,我们不得不为缓存选择一些边界大小,因为缓存有一个强引用,可能会阻止垃圾回收,所以有折衷的办法。我避免使用弱引用的一般原因是使用它们会非常强烈地将你构建的内容的行为绑定到垃圾回收器的行为上。这可能被视为个人口味的问题。

0
by

评论者:jafingerhut

2014年8月8日及之前的所有补丁,在一些关于Clojure的提交被作出后,已无法干净地应用到2014年8月29日的最新master分支上。在那之前,这些补丁是可以干净地应用的。

我尚未检查更新补丁的难易程度。

0
by

评论者:hiredman

我已经更新了 naive-lru-for-multimethods-and-protocols.diff,使其适用于当前的master。

0
by

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许将具有相同名称但内容不同的多个附件附加到同一个工单中,但这可能会让寻找特定补丁的人困惑,以及对于我那个评估补丁的程序(例如,检查它们是否能干净地应用和构建)来说也是如此。你介意移除较旧的补丁,或者用其他方式确保所有名称都是唯一的吗?

...