2024 Clojure 状态调查! 中分享你的想法。

欢迎到来!请查阅 关于 页面以获取有关此信息的工作方式。

0
协议
我们发现一个 PermGen 内存泄漏问题与协议方法和在 {{eval}} 中调用的多方法有关,因为这些方法使用了缓存。只有在缓存的值是一个类的实例(例如函数或 reify)时,此问题才会出现,而这个类是在 {{eval}} 中定义的。因此,扩展 {{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. 多方法泄漏

评估以下代码将运行一个循环,该循环会 evaled {{(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}}会话中,您将看到由evaling创建的类被卸载。

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


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


-    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=协议泄漏}
(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 用非常简单的LRU缓存替换了 multimethods 中的 methodCache,该缓存建立在 PersistentHashMap 和 PersistentQueue 上

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 实际上被用作缓存,我们不能在它满了之后简单地扔掉条目。

再次查看 de type 代码,看起来 MethodImplCache 被用作缓存,所以也许补丁是正确的。

如果我对任何事情有把握,那就是我不确定,希望有把握的人可以加入进来。

0

评论者:bronsa

我还没有查看你的补丁,但我可以确认,协议函数中的 MethodImplCache 只是作为缓存使用。

0

评论者:killme2008

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

我在 ticket 的代码中进行了测试,看起来一切正常。当 perm gen 几乎用完时,类将被卸载。

0

评论由:alexmiller 发布

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

0

评论由:hiredman 发布

我认为最直接的做法是将它们视为替代方案。我不是特别热衷于使用弱引用,但当然,不使用弱引用的话,我们必须为缓存选择一个限制大小,因为缓存具有强引用,可能会阻止垃圾回收,因此存在权衡。我之所以通常避免使用弱引用,是因为使用它们会将你所构建的内容的行为与垃圾回收器的行为紧密绑定,这可能是个人喜好的问题。

0

评论人:jafingerhut

在2014年8月29日对Clojure进行了一些提交后,所有于8月8日及之前的补丁在最新主分支上都无法干净利落地应用。在此之前,它们都能干净地应用。

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

0

评论由:hiredman 发布

我已经更新了 naive-lru-for-multimethods-and-protocols.diff 以适用于当前主分支。

0

评论人:jafingerhut

谢谢,Kevin。虽然JIRA允许将同名但内容不同的多个附件附加到同一个工单上,但这对寻找特定补丁的人来说可能会造成混淆,而且对于我的一个评估补丁是否可以干净应用并构建的程序而言也是如此。你介意删除旧的补丁,或者用其他方式确保所有名称都是唯一的吗?

...