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}} 并通过 {{M-x slime-connect}} 在 emacs 中连接到 slime。

要监控 PermGen 使用情况,您可以找到要监控的 Java 进程,使用 "{{jps -lmvV}}",然后运行 "{{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生成的类正在被卸载。

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

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

h2. 协议泄漏

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

{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 投票

由 hiredman 发布的评论:

我已经删除了所有附件,只保留了我的最新和最好的那个。

0 投票

由 killme2008 发布的评论:

我也更新了 multifn_weak_method_cache2.diff 补丁。

我认为使用弱引用缓存更好,因为我们必须为 multifn 维护一个缓存。当你有很多多功能时,会有很多 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 来映射 RefWrapper 周围的键到 IFn (#1)。该补丁使用 Util.equals() 进行(Java)相等性比较。RefWrapper 将它们包裹在 WeakReferences 中以避免 #5。基于 ReferenceQueue 的缓存清理用于防止 #4。

一些事情确实需要修复
- Util.equals() 应该是 Util.equiv()
- methodCache 和 rq 应该是 final
- 为什么 RefWrapper 有 obj 而且expect rq 可能是 null?
所有RefWrapper字段都应该是最终字段。
补丁中有空白符错误。

另一个完全不同的想法——与其缓存分发值,不如基于分发值的hasheq缓存,然后进行值相等性检查。那时可以使用WeakHashMap和没有RefWrapper。

此补丁未涵盖协议缓存。它在等待多方法情况看起来不错吗?

0 投票

由 killme2008 发布的评论:

嗨,alex,感谢你的审查。但最新的补丁是multifn_weak_method_cache2.diff。我会尽快根据你的审查更新补丁,但我有几个问题要解释。

1) 我将使用Util.equiv()而不是Util.equals()。但它们有什么区别?
2) 当RefWrapper作为ConcurrentHashMap中的键保留时,它将obj包装在WeakReference中。但尝试在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) 修正了空白符错误。
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) 请更新此 bug 票的描述,包括一个方法部分,描述我们所做的更改。

谢谢!

0 投票
...