內存泄漏的理解和分類
可達性分析算法來判斷對象是否是不再使用的對象,本質都是判斷一上對象是否還被引用,對于這種情況下,由于代碼的實現不同就會出現很多內存泄漏問題(讓JVM誤以為此對象還在引用,無法回收,造成內存泄漏)
內存泄漏(memory leak)
嚴格來說,只有對象不會再被程序用到了,但是GC又不能回收他們的情況,叫內存泄漏 對象X引用對象Y,X的生命周期比Y的生命周期長 那么當Y生命周期結束的時候,X依然引用著Y,這時候,垃圾回收期是不會回收對象Y的 如果對象X還引用著生命周期比較短的A,B,C,對象A對引用著對象a,b,c,這樣可能造成大量無用的對象不能被回收,進而占據了內存資源,造成內存泄漏,直到內存溢出 申請了內存用完了不釋放,如一共有1024M內存,分配了512M的內存一直不回收,那么可用內存只有512M,仿佛泄露了一部分,通俗講內存泄漏就是【占著茅坑不拉shi】
內存溢出(out of memory)
申請內存時,沒有足夠的內存可以使用 通俗一點講,一個廁所三個坑,有兩個站著茅坑不走(內存泄漏),剩下一個坑,廁所一示接待壓力大,這時一下來了兩個人,坑位(內存)不夠了,內存泄漏變成內存溢出了 內存泄漏和內存溢出的關系,內存泄漏增多,最終導致內存溢出
泄漏分類
經常發生 :發生內存泄露的代碼會被多次執行,每次執行,泄露一塊內存偶然發生 :在某些特定情況下才會發生一次性 :發生內存泄漏的方法只會執行一次隱式泄漏 :一直占著內存不釋放,直到執行結束,嚴格的說這個不算內存泄漏,因為最終釋放掉了,但如果執行時間特別長,會導致內存耗盡
Java內存泄漏的8種情況
靜態集合類
靜態集合類,如HashMap、LinkedList等,如果這些容器為靜態的,那么它們的生命周期與JVM程序一致,則容器中對象在程序結束這前將不能被釋放,從而造成內存泄漏,簡單而言,長生命周期的對象持有短生命周期對象的引用,盡管生命周期的對象不再使用,但是因為長生命周期對象持有它的引用而導致不能被回收。
public class MemoryLeak { static List list = new ArrayList ( ) ; public void oomTests ( ) { Object obj = new Object ( ) ; list. add ( obj) ; }
}
單例模式
單例模式和靜態集合導致內存泄漏的原因類似,因為單例的靜態特性,它的生命周期和JVM的生命周期一樣長,所以如果單例對象如果持有外部對象的引用,那么這個外部對象也不會被回收,那么就會造成內存泄漏。
內部類持有外部類
內部類持有外部類,如果一個外部類的實例對象的方法返回一個內部類的實例對象 這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄漏
各種連接,如數據庫連接,網絡連接和IO連接等
各種連接,如數據庫連接,網絡連接和IO連接等 在對數據庫進行操作的過程中,首先要建立與數據庫連接,當不再使用時,需要調用close方法來釋放與數據庫連接,只有連接被關閉后,垃圾回收器才會回收對應的對象 否則,在訪問數據庫的過程中,對Connection,Statement或ResultSet不顯性地關閉,將會造成大量的對象無法被回收,從而引起內存泄漏
public static void main ( String [ ] args) { try { Connection conn = null ; Class . forName ( "com.mysql.jdbc.Driver" ) ; conn = DriverManager . getConnection ( "url" , "username" , "password" ) ; Statement stmt = conn. createStatement ( ) ; ResultSet rs = stmt. executeQuery ( "...." ) } catch ( Exception e) { } finally { }
}
變量不合適作用域
變量不合理的作用域,一般而言,一個變量的定義的作用范圍大于其使用范圍,很有可能會造成內存泄漏,另一方面,如果沒有及時地把對象設置為null,很有可能導致內存泄漏的定義
public class UsingRandom { private String msg; public void receiveMsg ( ) { msg = readFromNet ( ) ; saveDB ( ) ; }
}
上面偽代碼,通過readFromNet方法把接收到的消息保存到變量msg中,然后調用saveDB方法把msg內存保存到數據庫,此時msg已經沒用了,由于msg的生命周期與對象的生命周期相同,此時msg不能回收,因此造成內存泄漏 實際msg變量可以放在receiveMsg方法內部 ,當方法用完,那么msg的生命周期就結束了,此時就可以回收了,另一種方法,使用完msg后,將msg=null,此時垃圾回收器也會回收msg內存空間
改變哈希值
當一個對象被存儲進HashSet集合中后,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改后的哈希值與最初存儲進HashSet集合中的哈希值就不同了,在這種情況下,即使用在contains方法使用該對象的當前引用作為的參數去HashSet集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從HashSet集合中單獨刪除當前對象,造成內存泄漏 這也是String設置成不可變類型的原因,可以放心把String存入HashSet,或把String當成HashMap的key值 當我們需要把自定義的類保存到散列表時,需要保證對象的hashCode不可變
public class ChangeHashCode1 { public static void main ( String [ ] args) { HashSet < Point > hs = new HashSet < > ( ) ; Point cc = new Point ( ) ; cc. setX ( 10 ) ; hs. add ( cc) ; cc. setX ( 20 ) ; System . out. println ( "hs.remove = " + hs. remove ( cc) ) ; hs. add ( cc) ; System . out. println ( "hs.size = " + hs. size ( ) ) ; }
}
class Point { int x; public int getX ( ) { return x; } public void setX ( int x) { this . x = x; } @Override public int hashCode ( ) { final int prime = 31 ; int result = 1 ; result = prime * result + x; return result; } @Override public boolean equals ( Object obj) { if ( this == obj) { return true ; } if ( obj == null ) return false ; if ( getClass ( ) != obj. getClass ( ) ) return false ; Point other = ( Point ) obj; if ( x != other. x) return false ; return true ; }
}
hs. remove = false
hs. size = 2
緩存泄漏
一旦對象引用放入緩存中,很容易遺忘,比如,之前項目在一次上線的時候,應用啟動奇慢直至夯死,因為代碼中會加載一個表中的數據到緩存(內存)中,測試環境只有幾百條數據,但是生產環境有幾百萬的數據。 對于這個問題,可以使用WeakHashMap代表緩存,此種Map的特點是,當除了自身有對key的引用外,此key沒有其他引用那么此map會自動丟棄此值。
public class MapTest { static Map wMap = new WeakHashMap ( ) ; static Map map = new HashMap ( ) ; public static void main ( String [ ] args) { init ( ) ; testWeakHashMap ( ) ; testHashMap ( ) ; } public static void init ( ) { String ref1 = new String ( "object1" ) ; String ref2 = new String ( "object2" ) ; String ref3 = new String ( "object3" ) ; String ref4 = new String ( "object4" ) ; wMap. put ( ref1, "cacheObject1" ) ; wMap. put ( ref2, "cacheObject2" ) ; map. put ( ref3, "cacheObject3" ) ; map. put ( ref4, "cacheObject4" ) ; System . out. println ( "String引用ref1,ref2,ref3,ref4消失" ) ; } public static void testWeakHashMap ( ) { System . out. println ( "WeakHashMap GC前" ) ; for ( Object o : wMap. entrySet ( ) ) { System . out. println ( o) ; } try { System . gc ( ) ; TimeUnit . SECONDS . sleep ( 5 ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } System . out. println ( "WeakHashMap GC后" ) ; for ( Object o : wMap. entrySet ( ) ) { System . out. println ( o) ; } } public static void testHashMap ( ) { System . out. println ( "HashMap GC前" ) ; for ( Object o : map. entrySet ( ) ) { System . out. println ( o) ; } try { System . gc ( ) ; TimeUnit . SECONDS . sleep ( 5 ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } System . out. println ( "HashMap GC后" ) ; for ( Object o : map. entrySet ( ) ) { System . out. println ( o) ; } }
}
String 引用ref1, ref2, ref3, ref4消失
WeakHashMap GC 前
object2= cacheObject2
object1= cacheObject1
WeakHashMap GC 后
HashMap GC 前
object4= cacheObject4
object3= cacheObject3
HashMap GC 后
object4= cacheObject4
object3= cacheObject3
監聽器和回調
如果客戶端在你實現API注冊回調,卻沒有顯示的取消,就會產生積聚,需要確保回調被當作垃圾回收的最佳方法是只保存它的弱引用,保存成WeakHashMap的鍵