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

评估以下代码将运行一个 evals {{(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*}} 时将该类添加到缓存中,从而防止该类被回收。

prefer-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,它提供了一个使用 PHashMap 和 PQueue 在 IPMap 接口后面构建的 LRU 缓存。

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

将 expand-method-impl-cache 更改为使用 MethodImplCache 的 map 情况下的 LRUCache

0

评论者:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 只是错误的,除非 MethodImplCache 真正被用作缓存,我们不能在它满了之后简单地丢弃条目。

再次查看 deftype 代码,它看起来 MethidImplCache 确实被用作缓存,所以也许这个补丁是好的

如果我对任何事情都有把握,那就是我不确定,所以希望有把握的人能介入

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
by

评论者:hiredman

我认为最直接的方法是将它们视为替代方案,我不是弱引用的大粉丝,但当然,如果不使用弱引用,我们必须为缓存选择一个边界大小,因为缓存有强引用,这可能会阻止垃圾回收,因此存在一些权衡。我避免使用弱引用的总体原因是使用它们会非常强烈地将你所构建的任何东西的行为绑定到垃圾回收器的行为,这可能是个人喜好问题。

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允许同一文件名但是内容不同的多个附件,但这可能会令寻找特定补丁的人感到困惑,以及对于我有一个评估补丁的脚本,比如它们是否可以干净地应用和构建。你介意移除较旧的补丁,或者以其他方式确保所有文件名都是唯一的吗?

...