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}}并使用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. 多方法泄露

评估以下代码将运行一个循环,该循环会eval{{(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 创建的类正在被卸载。

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


在 jstat 监控中,当使用的 PermGen 空间接近最大值时,将会出现长时间的暂停,随后它会降下,并在更多 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   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=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 投票
by

评论者:hiredman

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

0 投票

评论由:killme2008发表

我也更新了multifn_weak_method_cache2.diff补丁。

我认为使用弱引用缓存更好,因为我们必须为每个multifn保留一个缓存。当您有许多多函数时,内存中会有许多LRU缓存,它们在驱逐时将消耗太多内存和CPU。您无法为每个环境选择合适的LRU缓存阈值。
但我不具备任何支持我观点的基准数据。

0 投票

评论由:alexmiller发表

我打算将LRU缓存补丁放一边。我不认为可以发现适当的“正确”大小,而且我觉得扩展APersistentMap来构建这样的事情也很奇怪。

我认为更合理的是遵循用于其他缓存(如Keyword缓存)的相同策略——结合ConcurrentHashMap与WeakReferences和ReferenceQueue进行清理。我看不出有任何有力理由不与其他内部缓存采取相同的途径。

0 投票

评论由:alexmiller发表

稍微退步一点来思考这个问题....我们的需求是
1) 将分发值(可以是任何Object)映射到多方法函数(IFn)的缓存
2) 我们是否希望基于相等性还是身份来比较键?基于身份的比较打开了更多基于引用的缓存选项,并且对于大多数常见的分发类型(如Class、Keyword)是可行的,但减少了(通常是消除?)所有其他类型的缓存命中,其中值相等但不相同(例如字符串向量)
3) 对缓存的同时访问
4) 缓存不能无限制增长
5) 缓存不能保留对分发值(键)的强引用,因为键可能是另一个类加载器中加载的类的实例,这将阻止permgen中的GC

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

几件事 definitely 需要 fix
- Util.equals() 应该是 Util.equiv()
- methodCache 和 rq 应该为 final
- 为什么RefWrapper有obj且expect rq 可能是null?
- RefWrapper的字段都应该为final
- 补丁中的空白错误

另一种完全不同的想法——不是基于分发值进行缓存,而是基于分发值的hasheq进行缓存,然后基于值进行相等性检查。那时可以使用WeakHashMap而不需要RefWrapper。

这个补丁没有覆盖协议缓存。这是否在等待多方法情况良好后再进行?

0 投票

评论由:killme2008发表

嗨,alex,感谢您的审阅。但是最新的补丁是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中,并根据alex的审阅重构代码。
3) 修复了关于空白错误的bug。
4) 修复了协议函数中的PermGen泄漏。

0 投票

评论由:alexmiller发表

我与Brenton Ashworth重新审查了这个工单,并有以下评论

1) 我们需要一个性能测试来验证我们没有对多方法或协议调用性能产生负面影响。
2) 由于多方法缓存中存在关于null键的特殊情况,请验证现有的测试覆盖率中是否存在使用null分发值的现有示例测试。
3) 在Util$RefWrapper.getObj()中 - 为什么这个方法在最后返回this.ref?对于我来说,这个评论是否正确或者是否有任何实际作用并不清楚。
4) 在Util$RefWrapper.clearRefWrapCache()中 - 在那个if检查中k是否可能为null?如果不是,我们能省略这个检查吗?此外,如果您从entry set显式创建Iterator,您可以比在缓存上调用.remove()更有效地调用它。
5) 在core_deftype / MethodImplCache中,您似乎正在修改一个现在可变的字段,而不是那个努力保持不可变性的先前版本。我不清楚这个变化的含义,这让我感到担忧。我们不能使用不同的集合或代码来保持不可变吗?
6) 请更新此工单的描述,包括一个方法部分,以便描述我们正在进行的变更。

谢谢!

0 投票
...