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

欢迎!请参阅 关于 页面,了解有关如何使用本站的更多详细信息。

0
协议
我们已追踪到一种 PermGen 内存泄漏,这一问题出在调用协议方法和在 evaled 中定义的类(如函数或 reify)的多方法,因为这些方法使用了缓存。只有当缓存的值是一个在 evaled 中定义的类的实例时,才会出现此问题。因此,扩展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"])
               "-XX:+TraceClassUnloading"])
                "-XX:+TraceClassUnloading"])


您可以使用 {{lein swank 45678}} 并通过 {{M-x slime-connect}} 在 emacs 中连接。

要监视 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]
  '())

(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


解决方法是,在永久生成空间耗尽之前运行 {{prefer-method}},例如:

(prefer-method take* clojure.lang.Fn java.lang.Object)


然后,当使用的永久生成空间接近最大值时,在 {{lein swank}} 会话中,您将看到通过 eval 执行创建的类正在被卸载。

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


在 jstat 监控中,当使用的永久生成空间接近最大值时,会出现长时间的暂停,然后下降,并在发生更多 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回收。

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

协议泄露

使用协议方法的泄露类似地涉及缓存。如果您使用以下代码使用协议运行,您将看到基本上与多方法泄露相同的行为。

{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}} 类作为键。

解决方案:在永久生成空间耗尽之前在协议上运行 {{-reset-methods}},例如:

(-reset-methods ITake)


这行得通,因为 {{-reset-methods}} 用空的 MethodImplCache 替换了缓存。

解决方案:protocol_multifn_weak_ref_cache.diff

已过滤:

23 答案

0

评论由:[email protected] 发布

我认为最明显的解决方案是限制缓存的大小。向缓存中添加项已经不是最快的路径,所以可以做更多的工作来防止缓存无限期地变大。

这引出了一个问题:应该使用什么标准。保留前n个条目?保留最近最常使用的n项(这可能需要在快速缓存命中率路径中进行会计处理)?还是保留最近添加的n个项?

0

注释者:jsmorph

至少,或许一个开关可以禁用缓存——当然会带来明显的性能影响警告。

看起来 expensive LRU 逻辑可能是正确的选择,但也许不要完全启动,直到达到某个阈值。

0

注释者:alexmiller

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

0

注释者:adrianm

这就是我们一直在遇到PermGen空间异常的原因!这对我们来说是一个相当关键的bug - 我在我们的代码库中广泛使用了multimethods,这个异常会在运行时随机出现。

0

注释者:hiredman

最好将其分成两个问题,因为在非常抽象的层面上,这两个问题是“相同”的,但具体来说,它们是不同的(协议实际上并不与multimethods共享代码路径),将它们保存在一个问题中似乎是一个大而难以阅读的补丁的配方

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 代码,MethidImplCache 似乎真的被用作缓存,所以补丁可能没问题

如果我确定 anything,那就是我不确定,所以希望有确定想法的人能加入进来

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
by

评论者:jafingerhut

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

我没有检查更新补丁的可能难易程度。

0

注释者:hiredman

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

0

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许将相同文件名但不同内容的多个附件附加到票据上,但这可能会让寻找特定补丁的人感到困惑,并且对于评估补丁是否可以干净安装并构建的程序来说也是如此。你介意删除旧的那个,或者以其他方式确保所有名称都是唯一的吗?

...