2024年Clojure调查问卷中分享您的观点!

欢迎!请参阅关于页面,了解更多关于这一工作的信息。

0
协议
我们已经追踪到一种 PermGen 内存泄漏,它源于在 {{eval}} 中调用的协议方法和多方法,因为这些方法使用缓存。只有在缓存的值是定义在 {{eval}} 中的类(例如函数或 reify)的实例时,问题才会出现。因此,扩展 {{IFn}} 或在 {{IFn}} 上分派多方法可能是触发器。

*重现方式:* 我发现最简单的方法是设置 "{{-XX:MaxPermSize}}" 至一个合理的值,这样你不必等待 too long for the 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 使用情况,您可以找到要监视的 Java 进程使用 "{{jps -lmvV}}",然后运行 "{{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'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. 协议泄漏

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

{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空间异常的原因就在于此!这是一个对我们来说相当关键的bug - 我在我们的代码库中广泛使用多方法,而这个异常会在运行时随机出现。

0

注释由:hiredman做出

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

0

注释由:hiredman做出

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

0

注释由:hiredman做出

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

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

将 expand-method-impl-cache 案例更改为使用 LRUCache。

0

注释由:hiredman做出

我认为我的补丁 naive-lru-for-multimethods-and-protocols.diff 可能是错误的,除非 MethodImplCache 的确是被用作缓存,不能随意丢弃条目当它满了。

再次查看 deftype 代码,看起来 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做出

我认为最直接的做法是将它们视为替代品。尽管我不是特别热衷于使用弱引用,但如果不使用弱引用,我们必须为缓存选择一个边界大小,并且缓存有一个强引用,这可能阻止垃圾收集,因此这里存在权衡。我不倾向于使用弱引用的原因通常是将你所构建的任何行为与垃圾收集器的行为紧密相关。这可能是个人品味的考量。

0

评论者:jafingerhut

2014年8月8日及以前的所有补丁,在2014年8月29日对Clojure进行了某些提交之后,已无法干净地应用于最新的master。在这之前,它们是可以干净应用的。

我尚未检查更新补丁的难易程度。

0

注释由:hiredman做出

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

0

评论者:jafingerhut

谢谢,Kevin。尽管JIRA允许对同一文件名但内容不同的多个附件进行操作,但这对寻找特定补丁的人来说可能有些令人困惑,尤其对于我拥有的评估补丁的程序而言,例如检查它们是否可以干净地应用和构建。你介意删除旧的那个,或者以某种方式使所有名称唯一吗?

...