2024年Clojure状态调查!中分享您的想法。

欢迎!请参见关于页面了解有关此内容的一些更多信息。

0
协议
我们发现了一个PermGen内存泄露,它仅限于在eval内部调用的协议方法和多方法,这是因为这些方法使用缓存。只有在缓存的值是类实例(如函数或reify)且在eval内部定义时,问题才会出现。因此,扩展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}}并使用emacs中的slime通过{{M-x slime-connect}}进行连接。

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

h2. 多方法泄露

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

{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 类,然后在调用 {{take*}} 时将其添加到缓存中,从而阻止类被 GC。

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

h2. 协议泄露

与协议方法相关的泄露也涉及到一个缓存。如果您使用协议运行以下代码,您将看到与多方法泄露几乎相同的行为。

{code:title=protocol leak}
(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}} 类作为键。

* workaround:* 在 PermGen 空间用完之前,在协议上运行 {{-reset-methods}},例如:

(-reset-methods ITake)


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

* patch:* protocol_multifn_weak_ref_cache.diff

*Screened by:*

23 答案

0
by

评论作者:[email protected]

我认为最明显的解决方案是对缓存的大小进行限制。向缓存中添加项目已经是较慢的路径,所以我们可以再做更多工作,以防止缓存无限期地变得很大。

这确实提出了一个问题,即应该使用什么标准。保留前 n 个条目?保留最近最常使用的前 n 个(这需要在快速缓存命中路径中进行簿记)?还是保留最近添加的前 n 个?

0
by

评论者:jsmorph

至少,也许可以切换到禁用缓存——带有明显的性能影响警告。

看起来昂贵的LRU逻辑可能是最好的选择,但是也许在某个阈值之前不要完全启动。

0
by

评论者:alexmiller

关于在生产中看到这个问题的报告
https://groups.google.com/forum/#!topic/clojure/_n3HipchjCc

0
by

评论者:adrianm

这就是我们为什么总是在遇到PermGen空间异常!这是一个对我们来说相当关键的错误——我在我们的代码库中广泛使用了多方法,这个异常将在运行时随机出现。

0
by

评论者:hiredman

可能最好将这个问题分成两个问题,因为在非常抽象的层面上,这两个问题是“相同的”,但在具体层面上它们是不同的(协议实际上并不与多方法共享代码路径),将它们保留在一个问题中似乎是生成一个大而难读补丁的配方

0
by

评论者:hiredman

naive-lru-method-cache-for-multimethods.diff 用非常简单的LRU缓存替换了multimethods中的methodCache,该缓存建立在PersistentHashMap和PersistentQueue之上。

0

评论者:hiredman

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

将 MultiFn 的方法缓存改为使用 LRUCache。

将 expand-method-impl-cache 改为使用 LRUCache 适用于 MethodImplCache 的 map 情况。

0

评论者:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 可能是错的,除非 MethodImplCache 真的是作为一个缓存来用,我们不能在它满了时就丢弃条目。

再看看 deftype 代码,看起来确实像 MethidImplCache 作为缓存在使用,所以也许补丁是好的。

如果我肯定任何事,那就是我不确定,所以希望某个确定的人能加入进来。

0

评论者:bronsa

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

0

评论者:killme2008

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

我已经用票据中的代码测试了它,看起来没问题。当 perm gen 几乎全部使用完毕时,类将被卸载。

0

评论者:alexmiller

我不知道在这里怎么评估。多 fn_weak_method_cache.diff 是否取代了 naive-lru-for-multimethods-and-protocols.diff,或者这两种替代方法都在考虑之下?

0

评论者:hiredman

我认为最简单的方法是考虑它们作为替代品,我不是特别喜欢的弱引用,但当然不使用弱引用,我们不得不为缓存选择一个边界大小,并且缓存有一个强引用,这可能会阻止垃圾收集,因此存在权衡。我不使用弱引用的主要原因是在使用它们的情况下,你所构建的内容的行为会非常紧密地与垃圾收集行为相联系。这可能是个人品味的问题

0

评论者:jafingerhut

在2014年8月29日对Clojure进行了某些提交之后,所有截至2014年8月8日及以前的补丁不再干净地应用于最新的主版本。在那一天之前,它们都应用得很好。

我没有检查更新补丁的可能难易程度。

0

评论者:hiredman

我已经将naive-lru-for-multimethods-and-protocols.diff更新为适用于当前主版本

0

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许对同一个名称但有不同内容的票提交多个附件,但这可能会让人难以找到特定的补丁,并且对我来说,有一个程序会评估这些补丁,判断它们是否应用和构建顺利。你介意移除旧的补丁,或者以其他方式确保所有名称都是唯一的吗?

...