我们发现了 permgen 内存泄漏,该泄漏问题追踪到在内部使用缓存的内部协议方法和多方法细节,因为缓存的问题引入了问题。只有在缓存的值是内部定义的类实例(例如函数或reify)时,才会出现这个问题(如函数或reify)。因此,这将引发IFn扩展或多方法派发。
**重现步骤:**我发现测试这种问题最简单的方法是将“{{-XX:MaxPermSize}}"”设置到一个合理的值,以便等待 PermGen 空间填满不会很长时间,并使用“{{-XX:+TraceClassLoading}}"”和“{{-XX:+TraceClassUnloading}}"”来查看加载和卸载的类。
{code:title=leiningen项目.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 中连接。
要监视 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=多方法泄露}
(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空间接近最大值时,将会有长时间的暂停,然后它会下降,并在更多评估执行时再次开始增加。
- 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
**筛选器:**