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

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

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

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


一种解决方案是,在永久代空间耗尽之前运行 {{prefer-method}},例如:

(prefer-method take* clojure.lang.Fn java.lang.Object)


然后,当使用的永久代空间接近最大值时,在 {{lein swank}} 会话中,您将看到通过 eval' 创建的类正在卸载。

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


在 jstat 监视中,当使用的永久代空间接近最大值时会出现长时间的暂停,然后它将下降,并在发生更多 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     2   50.589   50.813
 32768.0  18254.2      283776.0     17243.9      6     2   50.589   50.813


{{defmulti}} 定义了一个使用分派值作为键的缓存。循环中的每个 eval 调用都定义了新的 foo 类,然后在调用 {{take*}} 时将其添加到缓存中,防止类被垃圾回收。

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}} 类作为键。

*解决方案:* 在永久代空间耗尽之前对协议运行 {{-reset-methods}},例如:

(-reset-methods ITake)


这有效,因为 {{-reset-methods}} 使用空的 MethodImplCache 替换了缓存。

*补丁:* protocol_multifn_weak_ref_cache.diff

*筛选器:*

23 个答案

0
by

评论者:hiredman

我删除了除了最新和最出色的附件之外的所有附件

0
by

由 killme2008 发表的评论:

我还更新了 multifn_weak_method_cache2.diff 补丁文件。

我认为使用弱引用缓存更好,因为我们必须为每个 multifn 维护一个缓存。当你有很多多函数时,内存中会有很多 LRU 缓存,并且它们在驱逐时将消耗过多的内存和 CPU。你无法在每个环境中都选择一个合适的 LRU 缓存阈值。
但我没有任何基准数据来支持我的观点。

0
by

由 alexmiller 发表的评论:

我将把 LRU 缓存补丁放在一边。我认为不可能找到一个“正确”的大小来设置它,并且我认为扩展 APersistentMap 来构建这样的事情也很奇怪。

我认为最好遵循用来处理其他缓存(如关键字缓存)的策略 - 结合 ConcurrentHashMap、WeakReferences 和 ReferenceQueue 以进行清理。我看不到不遵循其他内部缓存的路径的任何令人信服的理由。

0
by

由 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 应该是最终的
- 为什么 RefWrapper 有 obj 并且预期 rq 可能是 null?
- RefWrapper 字段都应该最终
- 补丁中的空白错误

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

此补丁不包括协议缓存。这是否只是等待多方法情况表现得更好?

0
by

由 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
by

由 killme2008 发表的评论:

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

1) 使用Util.equiv()替代Util.equals()
2) 将RefWrapper及其相关方法移至Util.java,并根据alex的审阅重构代码。
3) 修复了空白错误。
4) 修复了协议函数中的PermGen泄漏。

0
by

由 alexmiller 发表的评论:

我再次与Brenton Ashworth讨论了此工单,并有以下评论

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

谢谢!

0
...