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

欢迎!请参阅关于页面以获取更多关于如何使用此网站的信息。

0
协议
我们已经追踪到一个PermGen内存泄露问题,该问题出现在 protocol 方法和多方法在{{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}},并通过emacs中的{{M-x slime-connect}}连接。

要监控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


一种解决方案是在 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*}} 时将其添加到缓存中,防止类被 GC。

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

h2. 协议泄漏

与协议方法的泄漏同样涉及一个缓存。如果您使用协议运行以下代码,您将看到基本上与 multimethod 泄漏相同的操作。

{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空间异常!这对我们来说是一个相当关键的bug——我们在代码库中广泛使用多方法,此异常在运行时可能会随机出现。

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 更改为使用 LRUCache 对 MethodImplCache 的 map 情况。

0

评论人:hiredman

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

再次查看 deftype 代码,MethidImplCache 看起来确实被用作缓存,所以这个补丁可能没有问题。

如果我对任何事情有把握,那就是我不确定,所以我希望有人能做出回应。

0

评论由:bronsa 提出

我没有查看你的补丁,但我可以确认,protocol 函数中的 MethodImplCache 只被用作缓存。

0

评论由:killme2008 提出

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

我已经用票据中的代码测试过它,看起来很正常。当 perm gen 几乎用尽时,这些类将被卸载。

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 进行了一些修改后,不再干净地应用于最新主分支。在那之前它都是适用的。

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

0

评论人:hiredman

我已更新 naive-lru-for-multimethods-and-protocols.diff 以匹配当前主分支

0

评论者:jafingerhut

谢谢,Kevin。虽然 JIRA 允许多个带有相同文件名但内容不同的附件,但对于寻找特定补丁的人来说可能会造成混乱,而且对于我拥有的一个评估补丁是否适用且能干净构建的程序来说也是如此。您能否移除旧的,或者在某种方式让所有文件名都是唯一的?

...