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

欢迎!有关此如何运作的更多信息,请参阅 关于 页面。

0
协议
我们发现有一个 PermGen 内存泄漏,它与在 {{eval}} 中调用的协议方法和多方法有关,因为它们使用缓存。只有在要缓存的值是定义为 {{eval}} 内的一个类的实例(例如一个函数或 reify)时,这个问题才会出现。因此,扩展 {{IFn}} 或在 {{IFn}} 上调用多方法可能是触发条件。

*重现:* 我发现设置 "{{-XX:MaxPermSize}}" 为一个合理的值,这样你不需要等待太长时间等待 PermGen 空间填满,以及使用 "{{-XX:+TraceClassLoading}}" 和 "{{-XX:+TraceClassUnloading}}" 来查看被加载和卸载的类的方法来测试这个问题是 easiest。

{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])
  [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=协议泄露}
(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

评论由:killme2008 提出

我也更新了 multifn_weak_method_cache2.diff 补丁。

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

0

评论由:alexmiller 提出

我将 LRU 缓存补丁放到一边。我认为不可能找到一个“正确”的大小,而且对我来说,将 APersistentMap 扩展来构建这样的事情也似乎很奇怪。

我认为遵循其他缓存(如 Keyword 缓存)使用的相同策略更有意义 - 结合 ConcurrentHashMap 与 WeakReferences 和一个 ReferenceQueue 进行清理。我看不到不采取与其他内部缓存相同路径的任何有说服力的理由。

0

评论由:alexmiller 提出

退一步想想这个问题……我们的需求是
1) 将派发值(可以是任何对象)的缓存映射到多方法函数(IFn)
2) 我们希望键是基于相等性还是身份进行比较?基于身份的比较为基于引用的缓存选项打开了更多可能性,对于大多数常见的派发类型(如 Class、Keyword)来说是可行的,但对于所有其他类型,其中值可能是等价的但并非相同(例如字符串向量),它会减少( Often eliminates?)缓存命中
3) 缓存的并发访问
4) 缓存不能无限制地增长
5) 缓存不能保留对派发值(缓存键)的强引用,因为这些键可能是另一个 classloader 中加载的类的实例,这将防止 permgen 中的 GC

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

一些事情肯定需要修复
- Util.equals() 应该是 Util.equiv()
- methodCache 和 rq 应该是最终的
- 为什么 RefWrapper 有 obj 并且期待 rq 可能为 null?
- RefWrapper 字段都应该最终
- 补丁中的空白错误

完全不同的想法 - 不是缓存分发值,而是基于分发值的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) 修复了空白错误。
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
...