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

欢迎!请查看关于页面以获取更多关于如何使用此内容的信息。

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}}并与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 [])}}。

{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}}方法,完全清空缓存。

2. 协议泄露

协议方法的泄露同样涉及一个缓存。如果您使用协议运行以下代码,会看到与多方法泄露类似的行为。

{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}}类作为键。

*解决方案:* 在使用完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投票
by

由 hiredman 发布的评论:

naive-lru-method-cache-for-multimethods.diff 用一个非常简单基于 PersistentHashMap 和 PersistentQueue 构建的 LRU 缓存替换 multimethods 中的 methodCache。

0投票
by

由 hiredman 发布的评论:

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

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

更改 expand-method-impl-cache 以使用 LRUCache 针对 MethodImplCache 的 map 情况。

0投票
by

由 hiredman 发布的评论:

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 是错误的,除非 MethodImplCache 冷有真实作为缓存使用,我们不能简单地将其条目在满了时丢弃。

再次研究 deftype 代码,看起来 MethodImplCache 确实被用作缓存,所以补丁可能没问题。

如果我确定任何事,那就是我不确定,所以希望能有人出来发声。

0投票
by

评论者:bronsa

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

0投票
by

评论者:killme2008

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

我已经用库存代码测试了它,看起来不错。在permgen几乎全部用完时,类将被卸载。

0投票
by

由 alexmiller 发布的评论:

我不知道在这里评估哪一个。multifn_weak_method_cache.diff 是否取代了 naive-lru-for-multimethods-and-protocols.diff,还是这两个不同的方法都在考虑之中?

0投票

由 hiredman 发布的评论:

在我看来,最直接的方法是将它们视为替代方案。我不是特别喜欢 weakrefs,但当然,如果我们不使用 weakrefs,我们必须为缓存选择一个限制大小,缓存有一个强引用,这可能会阻止垃圾回收,因此存在一些权衡。我不太喜欢弱引用的一般原因是使用它们会将你正在构建的内容的行为与垃圾回收的行为紧密结合。这可能是一个个人品味的问题。

0投票

评论者:jafingerhut

从2014年8月29日起,对Clojure进行一些提交之后,所有日期截止到2014年8月8日及之前的补丁在最新的 master 上不再完全适用。在这之前它们是可以正常应用的。

我没有检查更新补丁可能有多容易或困难。

0投票

由 hiredman 发布的评论:

我已更新 naive-lru-for-multimethods-and-protocols.diff 以适用于当前 master。

0投票

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许为同一个问题 attachment 相同名称但内容不同的多个单一的补丁,这可能会让人在寻找特定补丁时感到困惑,而且对我来说,这也可能让我那种评估补丁的程序混乱。你介意删除旧版本,或者在某种方式使所有名称唯一吗?

...