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

欢迎!请参阅关于页面以了解更多关于此工作方式的信息。

0
协议
我们发现了一个PermGen内存泄漏,它归因于在eval中调用协议方法和多方法使用的缓存。只有在缓存的值是一个实例时,这个问题才会出现,比如在eval内部定义的类(如函数或reify)对应的实例。因此,扩展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中通过{{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. 多方法泄漏

以下代码的评估将运行一个循环,该循环会eval {{(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'ing创建的类正在被卸载。

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


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


-    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. 协议泄露

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

{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
by

评论人:[email protected]

我认为最明显的解决方案是对缓存大小进行限制。向缓存添加项已经不是最快的路径,所以我们可以做更多工作来防止缓存无限增长。

这提出了一个问题:应该使用什么标准。保留前 n 条条目?保留最近 n 次使用的(这需要在快速缓存命中路径中进行记录)?保留最近添加的 n 项?

0

评论人:jsmorph

至少,也许可以通过禁用缓存来实现切换——同时提醒,这将明显影响性能。

看似昂贵的LRU逻辑可能是正确的选择,但也许不在达到某个阈限量级时完全启动。

0

评论人:alexmiller

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

0

评论人:adrianm

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

0

评论人:hiredman

可能更好地将这些问题拆分为两个问题,因为从非常抽象的角度来看,两个问题“相同”,但具体来说却是不同的(协议实际上并没有与多方法共享相同的代码路径),将它们放在一个问题上看起来像是为一个大型的难以阅读的补丁做准备。

0

评论人:hiredman

naive-lru-method-cache-for-multimethods.diff 将多方法中的 methodCache 替换为基于 PersistentHashMap 和 PersistentQueue 的非常简单的 lru 缓存

0

评论人:hiredman

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

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

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

0

评论人:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 可能是完全错误的,除非 MethodImplCache 真正被用作缓存,我们不能在它满了之后随便删除条目。

再次查看 dectype 代码,看起来 MethodImplCache 真的被用作缓存,所以补丁也许是可以的

如果我确定任何事情的话,那就是我不确定,所以希望有明确想法的人能加入讨论

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

我认为最直接的方法是将它们视为替代品。我不是弱引用的超级粉丝,但当然,不使用弱引用,我们必须为缓存选择一个边界大小,并且缓存具有强引用,这可能会阻止GC,所以有一些折衷方案。我远离弱引用的一般原因是使用它们会将你构建的任何东西的行为与GC的行为紧密关联起来。这可能会被认为是一个个人品味的问题。

0

评论者:jafingerhut

2014年8月8日之前的所有补丁在2014年8月29日对Clojure进行了一些提交后,不再干净地应用于最新的master版本。在此之前它们都可以干净地应用。

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

0

评论人:hiredman

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

0

评论者:jafingerhut

谢谢凯文。虽然JIRA允许同一文件名但内容不同的多个附件,但这可能会让寻找特定补丁的人感到困惑,而且我对补丁进行评估的程序需要检查它们是否应用并且构建干净。您介意移除旧的补丁,或者以其他方式使所有名称都是唯一的吗?

...