2024年Clojure调查中分享你的想法!

欢迎!请查阅关于页面了解如何使用此系统的更多信息。

0 投票
协议
我们发现了在Protocol方法和多方法中使用内部 declared.cache 时的PermGen内存泄漏。只有在缓存的值是一个在{{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中的slime通过{{M-x slime-connect}}连接。

要监视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. 多方法泄漏

执行以下代码将运行一个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}}会话中,您将看到通过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}}方法,该方法将缓存完全清空。

协议泄露



与协议方法类似的泄露也涉及到一个缓存。如果您使用协议运行以下代码,您将看到与多方法泄露相似的行为。

{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 投票

评论者: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) 缓存不能无限制地增长
缓存不能保留调度值(缓存键)的强引用,因为这些键可能是另一个类加载器中加载的类的实例,这将在 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 并且 expect rq 可能是 null?
- RefWrapper 字段都应该声明为最终变量
- 补丁中的空白错误

另一个完全不同的想法——不是缓存调度值,而是根据调度值的 hasheq 缓存,然后在值上进行相等性检查。然后可以使用 WeakHashMap,无需 RefWrapper。

这个补丁没有涵盖协议缓存。这只是为了等待多方法案例变得良好吗?

0 投票
by

评论由: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 投票
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吗?如果不是,我们能否省略它?此外,如果您从entry set显式创建Iterator,您可以通过调用remove()而不是在缓存本身上调用remove()来更高效地调用它。
5) 在core_deftype / MethodImplCache中,您似乎正在修改一个现在是可变性的字段,而不是一直努力保持不可变性的先前版本。我不清楚这种更改的后果,这让我感到担忧。它可以使用不同的集合或代码来保持不可变性吗?
6) 请更新此工单的描述,包括一个方法部分,描述我们正在进行的更改。

谢谢!

0 投票
by
...