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 中连接 sleim。

要监控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) 是 "Current permanent space capacity (KB)",第二列 (PU) 是 "Permanent space utilization (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}}会话中,您将看到正在评估时创建的类正在被卸载。

[卸载类 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}}定义了一个使用分发值作为键的缓存。在循环中,每次评估调用都定义了一个新的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 投票
by

评论者:hiredman

我删除了除最新、最好的附件之外的所有附件

0 投票
by

由:killme2008发表的评论

我也更新了 multifn_weak_method_cache2.diff 补丁。

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

0 投票
by

由:alexmiller发表的评论

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

我认为更合理的是遵循其它缓存(如关键字缓存)使用的相同策略 - 组合ConcurrentHashMap与WeakReferences,以及用于清理的ReferenceQueue。我不认为有任何令人信服的理由不被其他内部缓存采取相同的路径。

0 投票
by

由: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。

一些绝对需要修复的事情
- Util.equals() 应该是 Util.equiv()
- methodCache和rq应该是final的
- 为什么RefWrapper有obj并且期望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)修复了空白错误。
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创建迭代器,您可以通过调用它调用.remove()而不是在缓存上调用.remove()来更有效地调用它。
5)在core_deftype / MethodImplCache中,看来您正在修改一个现可变的字段,而不是将之前版本做到了极致的不可变版本。我不清楚这种更改的影响,这让我感到担忧。它是否可以使用不同的集合或代码保持不可变?
6) 请更新此票据的描述,包括一个方法部分,其中描述我们正在进行的更改。

谢谢!

0 投票
...