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

欢迎!请参见关于 页面了解更多关于本网站如何工作的小信息。

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

要监视 PermGen 使用情况,请使用 "{{jps -lmvV}}" 查找要监视的 Java 进程,然后运行 "{{jstat -gcold +_<PROCESS_ID>_+ 1s}}"。根据 [jstat 文档中的 gcold 选项|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=多方法泄漏}
(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空间接近最大时,会有较长的暂停,然后会下降,当发生更多的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*}}时将其添加到缓存中,防止类被GC回收。

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

*解决方案:* 在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) 缓存调度值(可以是任何对象)到多方法函数(IFn)的映射
2) 我们希望键是基于相等性还是身份进行比较?基于身份的比较打开了更多基于引用的缓存选项,并且对于大多数常见的调度类型(类、关键词)来说是可接受的,但它减少了(通常消除了?)所有其他类型的缓存命中,对于这些类型中的值可能相等但不完全相同(例如字符串向量)
3) 缓存支持并发访问
4) 缓存不能无限制地增长
5) 缓存不能保留指向调度值(键)的强引用,因为键可能是另一个类加载器加载的类的实例,这将防止permgen中的GC

multifn_weak_method_cache.diff 使用了一个 ConcurrentHashMap (#3),它将 RefWrapper(#1)包围的键映射到 IFn。该补丁使用 Util.equals() (#2) 进行基于(Java)相等的比较。RefWrapper 用弱引用包裹它们,以避免 #5。基于 ReferenceQueue 的缓存清理用于防止 #4。

有一些事情确实需要修复
- Util.equals() 应该使用 Util.equiv()
- methodCache 和 rq 应该是最终的
- 为什么 RefWrapper 有 obj 并且期望 rq 可能是 null?
- RefWrapper 字段都应该声明为 final
- 补丁中的空格错误

一个完全不同的想法 - 不是缓存分派值,而是根据分派值的哈希值缓存,然后在值上进行相等性检查。这样就可以使用 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()
将 RefWrapper 及其相关的方法移动到 Util.java,并根据 alex 的审查重构代码。
修复了空格错误。
修复了协议函数中的 PermGen 泄露问题。

0

评论者:alexmiller

Brenton Ashworth 和我再次审查了这张票,有以下评论

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

谢谢!

0
通过
...