原文鏈接:http://nius.me/classloader-memory-leak/
對於j2ee項目,一直使用eclipse的wtp,每次修改代碼后,可以自動熱部署。簡單的項目wtp似乎沒什么問題,但一旦項目代碼稍微多一點,就很容易出現各種莫名其妙掉掛的現象,不得不整個重啟tomcat服務器,這個時候就很痛苦了。
於是,我換用了maven的jetty插件,啟動一個輕量級的jetty服務器,這下熱部署似乎沒那么多問題了!但是jetty似乎在熱部署幾次之后,也仍然會崩潰!這是什么情況!tomcat和jetty到底發生了什么?jetty的崩潰最常看見的異常就是OutOfMemoryException,Perm區的內存占滿了。
short version
不要慌!為了節省時間,先上解決方案:classloader-leak-prevention。
- 在maven中添加如下配置:
<dependency> <groupId>se.jiderhamn</groupId> <artifactId>classloader-leak-prevention</artifactId> <version>1.9.3</version> </dependency>
- 在web.xml__頂部__添加一個listener
<listener> <listener-class> se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor </listener-class> </listener>
- 去西湖玩一天
long version
看看Perm區到底都是些啥
今天實在忍不了了,上Visual VM,監視一下Perm區的情況。
果然,reload幾次之后,Perm的使用量蹭蹭的往上漲。看看Perm區都是啥
怎么這么多char[]呀,好像也看不出什么,還是dump一下吧。中間翻閱了一些博客,覺得應該是class loader的問題。oql查詢一下。(這里因為是jetty,所以查詢了jetty的WebAppClassLoader,如果使用tomcat或者其他容器,應該有對應的Loader,可以通過Saved Queries->PermGen Analysis->ClassLoader Types看一下有哪些ClassLoader)
上圖是剛啟動的時候,WebAppClassLoader只有一個實例。經過兩次reload,嘿,問題來了。如下圖:
居然出現了3個WebAppClassLoader實例,前兩個Loader都沒有銷毀!喲呵呵,gc銷不掉,一會就進Perm區了,然后多幾次reload,Perm區再大也都撐滿了。
發生了什么,gc不靠譜?
首先,我們回憶一下gc的運作過程。通過minor gc,清理eden區(eden generation)的沒有被引用的對象,活下來的進入suvivor區,接下來的minor gc會讓對象在兩個suvivor里面倒騰倒騰,挺過幾次的進入old區,這里面進行的就是full gc了,耗時長,如果old區還挺了好幾次,就會進入Perm區。Perm里面發生的也是full gc.
一個普通對象,只要沒有引用了,就一定會在某一次gc被回收。那么ClassLoader進入了Perm,說明在reload的時候,雖然程序取消了對Classloader的直接引用,但是仍然有其他對象間接的引用了ClassLoader。其實如果單純的僅僅是ClassLoader一個對象,也就罷了,但是ClassLoader並不是一個普通的對象。
任何一個Java類,都是通過某個ClassLoader加載的。通過這個ClassLoader加載的類的實例,會保存這個類對象的引用,也就是MyClass.class這種。而類對象,會保留這個ClassLoader的引用。反過來,在ClassLoader中,也會保持對這個類對象的引用。(注意區分類對象MyClass.class,不是這個類的實例。好吧如果還是混淆了,我也不知道該怎么說清楚了)。關鍵在於,ClassLoader和類對象之間是雙向引用。
雙向引用有什么問題嘛?一般情況下沒有問題。因為如果ClassLoader的外界引用,和具體類對象的外界引用都消失了,那么這兩個對象都不可達了,都會被gc。但是在一些情況下,類對象可能不僅僅被這個類的實例保存,還可能被其他對象保存!如果這個對象是其他OtherClassLoader加載的呢?那意味着,如果這個對象不回收,那么其引用的類對象不會被回收,於是ClassLoader不會被回收,於是,所有ClassLoader加載的類對象都不會被回收!WebAppClassLoader會加載多少個類?如果你恰好使用的是Spring、Hibernate這種大家伙,嚯嚯。如果對此很感興趣,這里有一篇寫的很詳細的:Anatomy of a PermGen Memory Leak
怎么解決
你並不知道什么時候會出現某個外部對象會引用到類對象。所以解決問題的思路是,換一個ClassLoader。一開始的解決方案classloader-leak-prevention就是依賴這個思路的。核心代碼如下:
// Switch to system classloader in before we load/call some JRE stuff that will cause // the current classloader to be available for garbage collection Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); try { java.awt.Toolkit.getDefaultToolkit(); // Will start a Thread } catch (Throwable t) { error(t); warn("Consider adding -Djava.awt.headless=true to your JVM parameters"); } java.security.Security.getProviders(); java.sql.DriverManager.getDrivers(); // Load initial drivers using system classloader javax.imageio.ImageIO.getCacheDirectory(); // Will call sun.awt.AppContext.getAppContext()
讓這一段代碼運行在servlet初始化之前,在所有的listener之前。
下面是自己翻閱的一些資料:
英文的資料:http://java.jiderhamn.se/2012/03/04/classloader-leaks-vi-this-means-war-leak-prevention-library/ (在此鏈接可找到最新的maven地址以及源碼地址以及非maven得jar)
Git源碼地址:https://github.com/mjiderhamn/classloader-leak-prevention