请在 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 连接。

要监视 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 [])) 进行 eval。

{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 会话中,您将看到许多类似以下列出的 newly created and loaded classes 的行。


[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'ing创建的类正在被卸载。

[正在卸载数据用户$eval5950$foo__5951]
[正在卸载数据用户$eval3814]
[正在卸载数据用户$eval2902$foo__2903]
[正在卸载数据用户$eval13414]


在jstat监控中,当使用 PermGen 空间接近最大值时,会出现长时间的暂停,然后下降,并在激发更多eval'ing时再次增加。


-    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

评论者:hiredman

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

0

评论者:killme2008

我还更新了 multifn_weak_method_cache2.diff 补丁。

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

0

评论者:alexmiller

我打算把 LRU 缓存补丁放一放。我认为不可能找到合适的大小,而且对我来说,将 APersistentMap 扩展来构建如此东西似乎很奇怪。

我认为遵循与其他缓存(如关键字缓存)相同的策略更有意义——结合使用 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

嗨,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

已上传新的补丁'request_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()更有效地调用.remove()。
5) 在core_deftype / MethodImplCache中,你似乎正在修改一个现在可变性的字段,而不是之前努力保持不可变性的版本。我不清楚这个更改的含义,这让我感到担忧。它能否使用不同的集合或代码来保持不可变性?
6) 请更新这张票的描述,包括一个方法部分,描述我们所做的更改。

谢谢!

0
by
...