java内存泄漏案例_寻找内存泄漏:一个案例研究
java內存泄漏案例
一周前,我被要求修復一個有內存泄漏問題的webapp。 考慮到過去兩年左右的時間里我已經看到并修復了數百個泄漏,我想這有多難。
但是事實證明這是一個挑戰。 12小時后,我發現該應用程序中不少于5個漏洞,并設法修復了其中4個漏洞。 我認為這將是值得分享的經歷。 對于那些不耐煩的人–總而言之,我發現了
- MySQL驅動程序啟動后臺線程
- 重新部署時未卸載java.sql.DriverManager
- BoneCP從錯誤的類加載器加載資源
- 數據源已注冊到JNDI樹中,阻止了卸載
- 使用終結器的連接池與在單獨線程中運行的Google的參考隊列實現相關聯
當前的應用程序是一個簡單的Java Web應用程序,具有一些連接到關系數據庫的數據源,中間是Spring以將內容粘合在一起,并將簡單的JSP頁面呈現給最終用戶。 沒有魔術。 還是我想。 男孩,我錯了。
第一站 -MySQL驅動程序。 顯然,最常見MySQL驅動程序會在后臺啟動線程,以清理未使用和未關閉的連接。 到目前為止,一切都很好。 但是要注意的是,這個新創建的線程的上下文類加載器是您的Web應用程序類加載器。 這意味著在運行此線程并且您嘗試取消部署Webapp時,它的類加載器被甩了下來-加載了所有類。
顯然,從2012年7月到2013年2月,該錯誤已被發現。 您可以按照MySQL問題跟蹤器中的討論進行操作。 最終實現的解決方案是API的shutdown()方法,開發人員在重新部署之前應該知道要調用該方法。 好吧,我沒有。 我敢打賭,你們當中有99%的人也沒有。
在典型的Java Web應用程序中,有一個適合此類關閉掛鉤的好地方,即ServletContextListener類contextDestroyed()方法。 每次銷毀servlet上下文時,都會調用此特定方法,例如,這種情況通常發生在重新部署期間。 可能有相當多的開發人員意識到這個地方的存在,但是實際上有多少人意識到需要清理這個特定的鉤子呢?
回到該應用程序,該應用程序還沒有被修復。 我的第二個發現還與上下文類加載器和數據源有關。 使用com.jdbc.myslq.Driver時,它將自身注冊為java.sql.DriverManager類中的驅動程序。 同樣,這是有良好意圖的。 畢竟,這是您的應用程序用來確定在連接到數據庫URL時如何為每個查詢選擇正確的驅動程序的方法。 但是您可能會猜到一個陷阱:該DriverManager是在引導類加載器中加載的,而不是在Web應用程序的類加載器中加載的,因此在重新部署應用程序時無法將其卸載。
現在使事情真正變得奇怪的是,沒有一般的方法可以自行注銷驅動程序。 對您嘗試注銷的類的引用似乎是故意向您隱藏的。 在這種特殊情況下,我很幸運,應用程序中使用的連接池能夠注銷驅動程序。 萬一我記得問。 回顧過去的類似案例,這是我第一次看到在連接池中實現這種功能。 在此之前,我曾經不得不枚舉在DriverManager中注冊的所有JDBC驅動程序,以找出應該注銷的驅動程序。 我無法向任何人推薦這種體驗。
我想應該是這樣。 同一應用程序中的兩次泄漏已經可以忍受一個以上。 錯誤。 泄漏報告中盯著我的第三個問題是sun.awt.AppContext及其靜態字段mainAppContext。 什么? 我不知道該類應該做什么,但是我很確定手頭的應用程序沒有以任何方式使用AWT 。 因此,我啟動了一個調試器,以找出是誰加載了此類(以及為什么)。 另一個驚喜:它是com.sun.jmx.trace.Trace.out()。 您能想到com.sun.jmx類將之稱為sun.awt類的充分理由嗎? 我當然不能。 但是,該類堆棧源自連接池BoneCP 。 跳過導致該特定內存泄漏的代碼行的絕對零方式。 解? 我的ServletContextListener.contextInitialized()中的以下魔咒:
Thread.currentThread().setContextClassLoader(null); // Force the AppContext singleton to be created and initialized without holding reference to WebAppClassLoder sun.awt.AppContext.getAppContext();但是我仍然沒有做完:有些東西還在泄漏。 在這種情況下,我發現我們的應用程序將此數據源綁定到InitialContext() JNDI樹,這是一種很好的,標準化的方法,用于綁定對象以供將來發現。 但是,再次強調–使用這種好東西時,您必須通過在非常相同的contextDestroy()方法中從JNDI樹中解除綁定此數據源來清理自己。
好吧,到目前為止,我們遇到了相當合乎邏輯的問題,盡管很少見并且有些晦澀難懂的問題,但是有了一些推理和google-fu很快就解決了。 我的第五個也是最后一個問題就是這樣。 我仍然因為OutOfMemoryError:PermGen而使應用程序崩潰。 Plumbr和Eclipse MAT都向我報告說,罪魁禍首是把我的類加載器扣為人質的一個線程,名為com.google.common.base.internal.Finalizer。 “這家伙到底是誰?” –在黑暗吞沒我之前,我最后的想法是。 幾個小時和四杯咖啡后,我發現自己盯著三行:
emf.close(); emf = null; ds = null;很難準確地回憶一下在此期間發生的事情。 我對WeakReferences , ReferenceQueues , Finalizers , Reflection有遙遠的記憶,而我第一次在野外看到PhantomReference 。 即使到了今天,我仍然無法完全解釋為什么連接池使用終結器以及將終結器綁定到在單獨線程中運行的Google的參考隊列實現的原因以及目的。
我也無法解釋為什么關閉javax.persistence.EntityManagerFactory (在上面的代碼中命名為emf并保存在應用程序自己的類之一中的靜態引用中)的原因; 因此,我不得不手動使該引用無效。 以及對該工廠使用的數據源的類似靜態引用。 我確信Java的GC可以整天處理循環引用,但是,即使對于他來說,類,靜態引用,對象,終結器和引用隊列的魔力環似乎也太難了。 因此,這是我漫長的職業生涯中的第一次,我不得不取消Java參考。
我是一個謙虛的人,因此不能說我在短短12個小時內能最有效地找到以上所有方法。 但是我必須承認,過去三年來我幾乎一直在處理內存泄漏。 而且我什至擁有自己的創作Plumbr來幫助我(實際上,五分之四的泄漏是Plumbr在30分鐘左右的時間內發現的)。 但是要真正解決這些泄漏,我花了一個多日的時間。
總體而言-在Java EE和/或類加載器世界中,顯然有些問題。 開發人員必須記住所有這些掛鉤和配置技巧,這是不正常的,因為這根本不可能。 畢竟,我們喜歡用頭腦去做一些富有成效的事情。 而且,從與兩個流行的servlet容器( Tomcat和Jetty )捆綁在一起的變通辦法可以看出,問題很嚴重。 但是,解決該問題不僅需要緩解某些癥狀,還需要解決潛在的設計錯誤。
參考: 尋找 內存泄漏:我們的JCG合作伙伴 Nikita Salnikov Tarnovski在Plumbr Blog博客上的案例研究 。
翻譯自: https://www.javacodegeeks.com/2013/03/hunting-down-memory-leaks-a-case-study.html
java內存泄漏案例
總結
以上是生活随笔為你收集整理的java内存泄漏案例_寻找内存泄漏:一个案例研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为的遥遥领先,刷屏了苹果发布会
- 下一篇: 华为秋季全场景新品发布会定档9月25日