请在 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}} 并通过 {{M-x slime-connect}} 在 emacs 中与 slime 连接。

要监控 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)是 "当前 PermGen 空间容量(KB)" 和第二列(PU)是 "PermGen 空间利用率(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创建的类被卸载。

[卸载类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. 协议泄漏

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

{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
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用基于PersistentHashMap和PersistentQueue的非常简单的LRU缓存替换了多方法中的methodCache

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 真正被用作缓存,我们不能在它变满时随意丢弃条目。

再次查看 deftype 代码,它看起来 MethodImplCache 确实被用作缓存,所以也许这个补丁是好的。

如果我对任何事情有把握,那就是我不确定,所以我希望有人能发表意见。

0

评论者:bronsa

我没有看过你的补丁,但我可以确认 protocol 函数中的 MethodImplCache 只被用作缓存。

0

评论者:killme2008

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

我已经在 issue 的代码上进行过测试,看起来一切正常。当 perm gen 几乎用完时,类将会被卸载。

0

评论者:alexmiller

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

0

评论者:hiredman

我认为最直接的办法是把它们视为替代方案,我不是非常赞同使用弱引用,但当然,如果不使用弱引用,我们需要为缓存选择一个界限大小,因为缓存有一个强引用,它可能阻止垃圾回收,因此存在一些权衡。我一般不使用弱引用的原因是使用弱引用会非常强地将你构建的内容的行为和垃圾回收的行为联系起来。这可能是个人喜好的问题。

0

评论者:jafingerhut

截至2014年8月8日之前的所有补丁在2014年8月29日对Clojure进行了某些提交之后,就不能干净地应用于最新的master分支了。在那一天之前,它们是干净的。

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

0

评论者:hiredman

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

0

评论者:jafingerhut

谢谢,凯文。虽然JIRA允许对同一文件名但内容不同的票证附加多个附件,但这可能会让寻找特定补丁的人感到困惑,而且在我使用的评估补丁的批次中,该批次评估补丁是否可以干净应用和构建。你介意去掉旧的一个,或者通过其他方式使所有名称独特吗?

...