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

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

0
协议
我们发现存在一个PermGen内存泄露问题,该问题归因于{{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中的slime通过{{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}}会话中,您将看到由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
通过

评论由:hiredman发出

我删除了所有附件,除了最新的和最好的。

0

评论由:killme2008发出

我也更新了multifn_weak_method_cache2.diff补丁。

我认为使用弱引用缓存更好,因为对于每个多功能,我们需要保留一个缓存。当你有很多多功能时,内存中会有很多LRU缓存,并且它们在淘汰时将消耗太多的内存和CPU。你无法在每种环境下为LRU缓存选择一个合适的阈值。
但我没有任何基准数据来支持我的观点。

0

评论由:alexmiller发出

我将把LRU缓存补丁放到一边。我认为不可能找到一个“正确”的大小,而且让我觉得奇怪的是,无论如何扩展APersistentMap来构建这样的事情。

我认为更合理的是遵循其他缓存(如Keyword缓存)使用的相同策略 - 将ConcurrentHashMap与WeakReferences和ReferenceQueue组合在一起进行清理。我认为没有充分的理由不与其他内部缓存走相同的道路。

0

评论由:alexmiller发出

退一步来思考一下问题.......我们的需求是
1) 缓存调度值(可以是任何对象)到多功能方法(IFn)的映射
2) 我们是否希望根据相等性或标识符进行比较?基于标识符的比较打开了更多基于引用的缓存选项,并且对于大多数常见的调度类型(类、关键词)来说是合理的,但会减少(通常消除?)所有其他类型(如字符串向量)中值可能等价但不是同一个实例的缓存命中
3) 缓存的并发访问
4) 缓存不能无限增长
5) 缓存不能保留对调度值(缓存键)的强引用,因为键可能是加载在另一个类加载器中的类的实例,这将阻止permgen中的GC

multifn_weak_method_cache.diff使用ConcurrentHashMap (#3) 将键周围的RefWrapper映射到IFn (#1)。该补丁使用Util.equals () (#2) 进行基于(Java)相等性的比较。RefWrapper用WeakReferences包装它们以避免#5。基于ReferenceQueue的缓存清理用于防止#4。

有些问题确实需要修复
- Util.equals () 应该是 Util.equiv ()
- methodCache 和 rq 应该是final的
- 为什么RefWrapper有obj而expect rq可能为null?
- RefWrapper字段应该都是final的
- 补丁中的空白错误

一个完全不同的想法 - 用基于调度值的hasheq来缓存,然后对值进行相等性检查。然后可以只使用WeakHashMap,而不需要RefWrapper。

此补丁没有涵盖协议缓存。这是否只是在等待多功能案例看起来不错?

0

评论由:killme2008发出

你好,亚历克斯,感谢你的审阅。但最新的补丁是multifn_weak_method_cache2.diff。我将在你的审阅后尽快更新补丁,但我有几个问题需要解释。

1) 我将使用Util.equiv()而不是Util.equals()。但它们有什么区别?
2) 当RefWrapper被保留为ConcurrentHashMap的键时,它使用WeakReference将obj包装起来。但当尝试在ConcurrentHashMap中找到它时,它直接使用obj作为强引用,并使用传递null ReferenceQueue的方式创建它。请看multifn_weak_method_cache2.diff的第112行。简而言之,补丁在缓存中将分发值作为弱引用存储,但使用强引用进行缓存获取。

3) 如果基于hasheq缓存分发值,我们能避免hasheq值冲突吗?如果两个不同的分发值有相同的hasheq(或者为什么不会发生?),我们将陷入困境。

抱歉,这个补丁没有涵盖协议缓存,我将立刻添加。

0

评论由:killme2008发出

已上传新的补丁'protocol_multifn_weak_ref_cache.diff'。

1) 使用Util.equiv()而不是Util.equals()
2) 将RefWrapper及其关联方法移动到Util.java,并根据亚历克斯的审阅重构代码。
3) 修复了空白错误。
4) 修复了协议函数中的PermGen泄漏。

0

评论由:alexmiller发出

我和布雷顿·阿肖尔(Brenton Ashworth)再次审查了这个工单,以下是我们的一些评论:

1) 我们需要对多方法或协议调用的性能进行测试,以验证我们没有负面影响。
2) 因为在多方法缓存中关于null键存在特殊案例,请验证现有测试覆盖范围中是否有使用null分发值的现有示例测试。
3) 在Util$RefWrapper.getObj()中 - 为什么在最后返回this.ref?我不清楚注释是否正确,或者这以任何方式有用。
4) 在Util$RefWrapper.clearRefWrapCache()中 - 那里k是否可能等于null?如果不等于,我们可以省略这个检查吗?另外,如果你从entry set显式创建Iterator,你可以比调用cache本身的remove()更高效地调用它的remove()。
5) 在core_deftype / MethodImplCache中,似乎你正在修改现在是可变的字段,而不是先前拼命保持不可变的版本。我不清楚这个变化的意义,这让我很担心。它能使用不同的集合或代码来保持不可变吗?
6) 请更新此工单的描述,包括一个描述我们在进行的更改的方法部分。

谢谢!

0
...