请分享您的看法,参加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 +_<进程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=多方法泄漏}
(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回收。

prefere-method的解决方案有效,因为它调用了{{clojure.lang.MultiFn.preferMethod}},然后调用私有的{{MultiFn.resetCache}}方法,该方法完全清空缓存。

2. 协议泄露

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

{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

评论者:[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缓存替换了multimethods中的methodCache

0

评论者:hiredman

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

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

将 expand-method-impl-cache 修改为使用 LRUCache 对 MethodImplCache 的映射情况进行处理。

0

评论者:hiredman

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

再看看 deftype 代码,它看起来确实像 MethodImplCache 被用作缓存,所以补丁也许没事。

如果要确定的话,那就是我不确定,所以我希望有把握的人能发表意见。

0

评论人:bronsa

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

0

评论人:killme2008

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

我在问题票的代码上进行了测试,看起来不错。当永久生成空间几乎用尽时,类将被卸载。

0

评论者:alexmiller

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

0
by

评论者:hiredman

我认为最直接的方法是将它们视为替代方案。虽然我不是弱引用的大力支持者,但当然,如果不使用弱引用,我们必须选择一些缓存大小限制,因为缓存有一个强引用,这可能会阻止GC,因此存在权衡。我不赞成在一般情况下使用弱引用的原因是,使用弱引用会强烈地将你构建的任何内容的行为与GC的行为联系起来。这可能是个人喜好的问题。

0
by

评论者:jafingerhut

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

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

0
by

评论者:hiredman

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

0
by

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许对具有相同文件名但内容不同的补丁进行多次附件,但这可能对寻找特定补丁的人造成困惑,也可能对我的程序造成困扰,该程序评估补丁以确定它们是否应用纯净和构建纯净。你介意移除旧的,或者以某种方法使所有名称唯一吗?

...