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

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

0 张票
协议
我们发现了 permgen 内存泄漏,该泄漏问题追踪到在内部使用缓存的内部协议方法和多方法细节,因为缓存的问题引入了问题。只有在缓存的值是内部定义的类实例(例如函数或reify)时,才会出现这个问题(如函数或reify)。因此,这将引发IFn扩展或多方法派发。

**重现步骤:**我发现测试这种问题最简单的方法是将“{{-XX:MaxPermSize}}"”设置到一个合理的值,以便等待 PermGen 空间填满不会很长时间,并使用“{{-XX:+TraceClassLoading}}"”和“{{-XX:+TraceClassUnloading}}"”来查看加载和卸载的类。

{code:title=leiningen项目.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 中连接。

要监视 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=多方法泄露}
(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评估创建的类正在被卸载。

[卸载类 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}}定义了一个使用分发值作为键的缓存。循环中的每个eval调用定义了一个新的foo类,然后在调用{{take*}}时将其添加到缓存中,防止该类被GC回收。

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

2. 协议泄露

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

{code:title=protocol leak}
(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 张票
by

评论者:jsmorph

至少可以考虑切换到禁用缓存,但要注意这会明显影响性能。

看起来昂贵的LRU逻辑可能是一个不错的选择,但也许直到跨越某个阈值之前不要完全启用它。

0 张票
by

评论者:alexmiller

从邮件列表中看到一个在生产环境中遇到此问题的报告
https://groups.google.com/forum/#!topic/clojure/_n3HipchjCc

0 张票
by

评论者:adrianm

这就是为什么我们会遇到PermGen空间异常!这是一个对我们来说相当关键的错误 - 我在代码库中广泛使用了多方法,该异常将在运行时随机出现。

0 张票
by

评论者:hiredman

可能最好将其分为两个问题,因为这两个问题的抽象级别上是“相同”的,但具体上是不同的(协议实际上并不与多方法共享代码路径),将它们放在一起在一个问题中似乎会使得一个巨大的难以阅读的补丁

0 张票
by

评论者:hiredman

naive-lru-method-cache-for-multimethods.diff用基于PersistentHashMap和PersistentQueue的非常简单的LRU缓存替换了多方法中的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 代码,MethodImplCache 看起来确实被用作缓存,所以也许补丁是好的

如果我能肯定任何事情,那就是我不确定,所以希望有信心的人可以发表意见

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 张票

Jafingerhut 发布的评论:

在 2014 年 8 月 29 日对 Clojure 进行了一些提交后,8 月 8 日及之前的所有补丁不再适用于最新的 master 版本。在此之前,这些补丁都能顺利应用。

我尚未检查更新这些补丁是否容易或困难。

0 张票

评论者:hiredman

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

0 张票

Jafingerhut 发布的评论:

谢谢,Kevin。虽然 JIRA 允许一个工单有多个相同文件名但内容不同的附件,但对于寻找特定补丁的人来说可能会造成混淆,我有一个程序会评估补丁,如它们是否可应用和是否干净地构建。你介意移除旧的补丁,或者以某种方式使所有文件名唯一吗?

...