我们追踪到一个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}}并与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. 多方法泄露
评估以下代码会运行一个循环,该循环解析{{(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}}方法,完全清空缓存。
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
*审核者:*