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. 多方法泄漏

评估以下代码将运行一个循环,该循环评估{{(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)


然后,在{{lein swank}}会话中,当使用的PermGen空间接近最大值时,你会看到由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
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用一个非常简单的、基于PersistentHashMap和PersistentQueue构建的LRU缓存来替换多方法中的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 的映射案例。

0
by

评论人:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 可能是错误的,除非 MethodImplCache 确实被用作缓存,我们不能当它满了时就丢掉条目。

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

如果我对任何事有把握,那就是我不确定,所以我希望有人来确定

0
by

评论者:bronsa

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

0
by

评论者:killme2008

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

我用票据中的代码测试了它,看起来很正常。当 perm gen 快要用完时,类将被卸载。

0
by

评论人: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

谢谢,Kevin。虽然JIRA允许将具有相同文件名但内容不同的多个附件添加到同一个问题,但这可能会让查找特定补丁的人感到困惑,还有我有一个程序会评估补丁,例如它们是否干净地应用和构建。你介意删除旧的那个,或者以其他方式使所有名称唯一吗?

...