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

欢迎!请访问关于页面以获取更多信息关于这是如何在工作的。

0
协议
我们已追踪到一个永久代内存泄漏,该泄漏是由于在 {{eval}} 中调用的协议方法和多方法调用中使用的缓存所导致的。只有在缓存的值是定义在 {{eval}} 内的类的实例(如函数或 reify)时,该问题才会出现。这意味着扩展 {{IFn}} 或在 {{IFn}} 上分配多方法是可能触发因素的。

*重现:* 我找到测试此问题的最简单方法是设置 "{{-XX:MaxPermSize}}" 为一个合理值,这样您不必等待太久等待永久代空间填满,并且使用 "{{-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 中与 slime 连接。

要监控永久代的使用情况,您可以使用 "{{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)”是第一列,“永久空间利用率(KB)”是第二列。VisualVM也是监控此的工具。

h2. 多方法泄漏

求值以下代码将运行一个运行 {{(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__]


这些行将停止,一旦永久代空间填满。

在 jstat 监控中,您将看到使用的永久代空间(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

评论者:[email protected]

我认为最明显的解决方案是对缓存的大小进行限制。向缓存中添加项目已经是速度较慢的路径了,因此可以对缓存实施更多的工作,以防止其无限制地变大。

这确实引发了一个问题,那就是应该使用什么标准。保留前 n 项?保留最近 n 次使用的项(这需要在快速缓存命中路径中进行记录)?还是保留最近 n 次添加的项?

0

评论者:jsmorph

至少,或许可以通过切换来禁用缓存,但要注意这将对性能产生明显的影响。

看起来,昂贵的LRU逻辑可能是最佳选择,但可能等到某个阈值被越过后再完全启动它。

0

评论者:alexmiller

从邮件列表中看到此问题在生产中的报告
https://groups.google.com/forum/#!topic/clojure/_n3HipchjCc

0

评论者:adrianm

这就是我们遇到PermGen空间异常的原因!这对我们来说是一个相当关键的错误——我在我们的代码库中广泛使用了多方法,这个异常将在运行时随机出现。

0

评论者:hiredman

最好将其拆分为两个问题,因为从非常抽象的层面上讲,这两个问题是“相同”的,但在具体上它们是不同的(协议并没有与多方法共享代码路径),把它们放在一个问题上似乎是一个大修补程序难以阅读的配方

0

评论者:hiredman

naive-lru-method-cache-for-multimethods.diff 使用基于 PersistentHashMap 和 PersistentQueue 的非常天真 LRU 缓存替换了 multimethods 中的 methodCache

0

评论者:hiredman

naive-lru-for-multimethods-and-protocols.diff 创建了一个新的类 clojure.lang.LRUCache,该类通过 IPMap 接口使用 PHashMap 和 PQueue 构建一个 LRU 缓存。

将 MultiFn 的方法缓存更改为使用 LRUCache。

将 expand-method-impl-cache 更改为使用 LRUCache 为 MethodImplCache 的映射情况进行优化。

0

评论者:hiredman

我怀疑我的补丁 naive-lru-for-multimethods-and-protocols.diff 是错误的,除非 MethodImplCache 的确被用作缓存,我们不能在它满了之后直接丢弃条目。

再看看 deftype 代码,看起来 MethodImplCache 的确被用作缓存,所以补丁可能没问题

如果我有什么可以确定的,那就是我不确定,所以我希望有把握的人可以回应

0

评论者:bronsa

我没有查看你的补丁,但我可以确认 protocol 函数中的 MethodImplCache 只被用作缓存

0

评论者:killme2008

我开发了一个新的补丁,将 MultiFn 中的 methodCache 转换为使用 WeakReference 来存储分派值,并在必要时清除缓存。

我已经在机票代码中测试了它,看起来很正常。当 perm gen 几乎用完时,这些类将被卸载。

0

评论者:alexmiller

我不知道在这里评估哪个。multifn_weak_method_cache.diff 是否取代了 naive-lru-for-multimethods-and-protocols.diff,或者这些替代方法都在考虑之中?

0

评论者:hiredman

我认为最简单的方法是将它们视为替代品,我不是特别喜欢弱引用,但当然,不使用弱引用我们必须为缓存选择一个限制大小,因为缓存有一个强引用可能阻止垃圾回收,所以这里有一些权衡。我避开弱引用的一般原因是使用它们将你正在构建的行为与垃圾回收的行为紧密关联,这可能是个人喜好的问题。

0

评论者:jafingerhut

自2014年8月29日对Clojure进行了某些提交后,所有2014年8月8日及以前的补丁已不再迭代的最新主分支干净。在那一天之前,它们都可以干净应用。

我没有检查更新补丁可能有多容易或多难。

0

评论者:hiredman

我已经更新了naive-lru-for-multimethods-and-protocols.diff以适用于当前的最新版主分支。

0

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许对具有相同文件名但内容不同的票据进行多个附件,但这可能会让寻找特定补丁的人感到困惑,而且对于我拥有的一个评估补丁的程序来说,它评估补丁是否能干净地应用和构建。您介意将其删除吗,或者以其他方式确保所有名称都是唯一的吗?

...