我们发现了一个PermGen内存泄露,它仅限于在eval内部调用的协议方法和多方法,这是因为这些方法使用缓存。只有在缓存的值是类实例(如函数或reify)且在eval内部定义时,问题才会出现。因此,扩展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 []))}}进行eval。
{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}} 方法,该方法完全清空了缓存。
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}} 类作为键。
* workaround:* 在 PermGen 空间用完之前,在协议上运行 {{-reset-methods}},例如:
(-reset-methods ITake)
这有效,因为 {{-reset-methods}} 将缓存替换为空的 MethodImplCache。
* patch:* protocol_multifn_weak_ref_cache.diff
*Screened by:*