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}}连接。

要监控PermGen的使用,您可以使用"{{jps -lmvV}}"找到要监控的Java进程,然后运行"{{jstat -gcold +_<PROCESS_ID>_+ 1s}}"。根据[jstat文档|,“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   -     -  4     0    0.000   0.129
 32000.0  31914.0  365952.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}},直到PermGen空间完全用尽,例如:

(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*}}时,该类被添加到缓存中,防止类被垃圾回收。

prefer-method解决方法有效,因为它调用了{{clojure.lang.MultiFn.preferMethod}},该函数调用私有的{{MultiFn.resetCache}}方法,该方法会完全清空缓存。

h2. 协议泄露

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

{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

评论者:[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缓存替换了多方法中的methodCache。

0

评论者:hiredman

文件“naive-lru-for-multimethods-and-protocols.diff”创建了一个新的类 clojure.lang.LRUCache,该类提供了一个使用 PHashMap 和 PQueue 在 IPMap 接口后面构建的 lru 缓存。

将 MultiFn 修改为使用 LRUCache 进行方法缓存。

将 expand-method-impl-cache 修改为使用 LRUCache 进行 MethodImplCache 的 map 用例。

0
by

评论者:hiredman

我认为我的补丁 naive-lru-for-multimethods-and-protocols.diff 是错误的,除非 MethodImplCache 真正被用作缓存,否则我们不能在它满了之后乱扔条目。

再次查看 deftype 代码,MethidImplCache 似乎确实被用作缓存,所以这个补丁可能没问题。

如果我对任何事情有信心,那就是我不确定,所以希望有人能站出来。

0
by

评论者:bronsa

我没有看过你的补丁,但我可以确认,在协议函数中的 MethodImplCache 只被用作缓存。

0
by

评论者:killme2008

我开发了一个新的补丁,将 MultiFn 中的 methodCache 转换为使用 WeakReference 作为调度值,并在必要时清理缓存。

我用票据中的代码测试了它,看起来很好。当 perm gen 几乎用完时,类将被卸载。

0
by

评论者:alexmiller

我不知道这里该如何评价。multifn_weak_method_cache.diff 是取代 naive-lru-for-multimethods-and-protocols.diff,还是这两个替代方案都在考虑中?

0
by

评论者:hiredman

我认为最直接的方法是将它们视为替代品,我不是弱引用的忠实粉丝,但当然,如果不使用弱引用,我们不得不为缓存选择一些边界大小,而缓存有一个强引用,这可能会阻止gc,因此存在折衷。我避开弱引用的一般原因是使用它们会将你正在构建的内容的行怍与gc的行为紧紧联系在一起。这可能是个人喜好的问题。

0

评论者:jafingerhut

在2014年8月29日对Clojure进行了一些提交后,所有日期为2014年8月8日及以前的补丁不再干净地应用到最新的master分支上。在那一天之前,它们是可以干净地应用的。

我没有检查更新补丁的难易程度。

0

评论者:hiredman

我已经更新了naive-lru-for-multimethods-and-protocols.diff,使其适用于当前master。

0

评论者:jafingerhut

谢谢,Kevin。虽然JIRA允许为具有相同文件名但内容不同的票据添加多个附件,但这可能会让寻找特定补丁的人感到困惑,并且对于我有一个用来评估补丁是否可以从源代码干净构建的程序来说,这也是一个问题。你介意移除旧的补丁,或者以某种方式让所有文件名都是独一无二的吗?

...