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

欢迎!有关本站如何运作的更多信息,请参阅关于页面。

0
协议
我们已经在协议方法和evaled内部调用的多方法中找到了PermGen内存泄漏,因为这些问题方法使用了缓存。只有当被缓存的值是evaled内部定义的类(如函数或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)是当前永久空间容量(以KB计量),第二列(PU)是永久空间利用率(以KB计量)。VisualVM也是监控这种情况的一个很好的工具。

h2. 多方法泄漏

评估以下代码将运行一个循环,该循环执行{{(take* (fn foo []))}}。

{code:title=multimethod leak}
(defmulti take* (fn [a] (type a)))

(defmethod take* clojure.lang.Fn [a])
  [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}}会话中,您将看到通过评估所创建的类正在被卸载。

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


在jstat监控中,当使用的PermGen空间接近最大值时,会出现长时间的暂停,然后它会下降,并在再次发生评估时开始增加。


—   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}}定义了一个使用分派值作为键的缓存。循环中的每个评估调用定义一个新的foo类,然后在调用{{take*}}时将其添加到缓存中,从而防止该类被GC。

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

h2. 协议泄露

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

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

评论者:jsmorph

至少,或许可以切换到禁用缓存的功能——但要注意这将对性能产生影响。

看起来昂贵的 LRU 逻辑可能是正确的方向,但也许不要在达到某个阈值之前完全启动。

0
by

评论者:alexmiller

从邮件列表中看到的生产中存在的报告
https://groups.google.com/forum/#!topic/clojure/_n3HipchjCc

0
by

评论者:adrianm

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

0
by

评论者: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的map情况。

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

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

谢谢,Kevin。虽然JIRA允许对具有相同文件名但不相同内容的票据添加多个附件,但这对寻找特定补丁的人来说可能会造成混淆,并且对我来说有一个评估补丁的程序,它会检查它们是否适用且能否干净地构建。您介意删除旧的版本,或者以其他方式使所有名称都是独一无二的吗?

...