要了解Java中的內存泄漏,首先就得知道Java中的內存是如何管理的。java
在Java程序中,咱們一般使用 new 爲對象分配內存,而這些內存空間都在堆上。程序員
Java判斷對象是否能夠回收使用的而是可達性分析算法。算法
這個算法的基本思路就是經過一系列名爲 "GC Roots" 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的,下圖對象 object5, object6, object7 雖然有互相判斷,但它們到 GC Roots 是不可達的,因此它們將會斷定爲是可回收對象。數據庫
在 Java 語言中,可做爲 GC Roots 對象的包括以下幾種:編程
Java 中的內存泄漏,廣義並通俗的說,就是:再也不會被使用的對象的內存不能被回收,就是內存泄漏。緩存
Java 中的內存泄漏與 C++ 中的表現有所不一樣。網絡
在 C++ 中,全部被分配了內存的對象,再也不使用以後,都必須程序員手動的去釋放他們。可是在 Java 中,咱們不用本身釋放內存,無用的內存由 GC 自動清理,這也極大的簡化了咱們的編程工做。但實際有時候一些再也不會被使用的對象在 GC 看來不能被釋放就會形成內存泄漏。dom
對象都是有生命週期的,有的長,有的短。若是長生命週期的對象持有短生命週期的引用,就極可能會出現內存泄漏。例如:jvm
public class Test { Object object; public void method() { object = new Object(); // ... } }
這裏的 object 實例,其實咱們指望它只做用於 method() 方法中,且其餘地方也不會再用到它,可是當 method() 方法執行完以後,object對象所分配的內存不會立刻被認爲是能夠被釋放的對象。只有在 Test 類建立的對象被釋放後纔會被釋放。嚴格地說,這就是一種內存泄漏。解決辦法就是將 object 做爲 method() 方法中的局部變量。固然也能夠在使用完 object 以後 將其置爲 null。性能
public class Test { Object object; public void method() { object = new Object(); // ... object = null; } }
這樣,以前 new Object() 分配的內存就能夠被 GC 回收。
如HashMap、LinkedList等等。若是這些容器爲靜態的,那麼它們的生命週期與程序一致,則容器中的對象在程序結束以前將不能被釋放,從而形成內存泄漏。簡單而言,長生命週期的對象持有短生命週期對象的引用,儘管短生命週期的對象再也不使用,可是由於長生命週期對象持有它的引用而致使不能被回收。
static Vector v = new Vector(); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; }
在這個例子中,代碼棧中存在 Vector 對象的引用 v 和 Object 對象的引用 o 。在 For 循環,咱們不斷的生成新的對象,而後將其添加到 Vector 對象中,以後將 o 引用置空。問題是當 o 引用被置空後,若是發生 GC,咱們建立的 Object 對象是否可以被 GC 回收呢?答案是否認的。由於, GC 在跟蹤代碼棧中的引用時,會發現 v 引用,而繼續往下跟蹤,就會發現 v 引用指向的內存空間中又存在指向 Object 對象的引用。也就是說盡管o 引用已經被置空,可是 Object 對象仍然存在其餘的引用,是能夠被訪問到的,因此 GC 沒法將其釋放掉。若是在此循環以後, Object 對象對程序已經沒有任何做用,那麼咱們就認爲此 Java 程序發生了內存泄漏。
在對數據庫進行操做的過程當中,首先須要創建與數據庫的鏈接,當再也不使用時,須要調用close方法來釋放與數據庫的鏈接。只有鏈接被關閉後,垃圾回收器纔會回收對應的對象。不然,若是在訪問數據庫的過程當中,對Connection、Statement或ResultSet不顯性地關閉,將會形成大量的對象沒法被回收,從而引發內存泄漏。
通常而言,一個變量的定義的做用範圍大於其使用範圍,頗有可能會形成內存泄漏。另外一方面,若是沒有及時地把對象設置爲null,頗有可能致使內存泄漏的發生。
public class UsingRandom { private String msg; public void receiveMsg(){ readFromNet();// 從網絡中接受數據保存到msg中 saveDB();// 把msg保存到數據庫中 } }
如上面這個僞代碼,經過 readFromNet() 方法把接受的消息保存在變量 msg 中,而後調用 saveDB() 方法把 msg 的內容保存到數據庫中,此時 msg 已經就沒用了,因爲 msg 的生命週期與對象的生命週期相同,此時 msg 還不能回收,所以形成了內存泄漏。
實際上這個 msg 變量能夠放在 receiveMsg() 方法內部,當方法使用完,那麼 msg 的生命週期也就結束,此時就能夠回收了。還有一種方法,在使用完 msg 後,把 msg 設置爲 null,這樣垃圾回收器也會回收 msg 的內存空間。
若是一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即便那個外部類實例對象再也不被使用,但因爲內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會形成內存泄露。
當一個對象被存儲進 HashSet 集合中之後,就不能修改這個對象中的那些參與計算哈希值的字段了,不然,對象修改後的哈希值與最初存儲進 HashSet 集合中時的哈希值就不一樣了,在這種狀況下,即便在 contains 方法使用該對象的當前引用做爲的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會致使沒法從 HashSet 集合中單獨刪除當前對象,形成內存泄露。
public static void main(String[] args) { Set<Person> set = new HashSet<Person>(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孫悟空","pwd2",26); Person p3 = new Person("豬八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素! p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變 set.remove(p3); //此時remove不掉,形成內存泄漏 set.add(p3); //從新添加,竟然添加成功 System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素! for (Person person : set) { System.out.println(person); } }
內存泄漏的另外一個常見來源是緩存,一旦你把對象引用放入到緩存中,他就很容易遺忘,對於這個問題,可使用 WeakHashMap 表明緩存,此種 Map 的特色是,當除了自身有對 key 的引用外,此 key 沒有其餘引用那麼此 map 會自動丟棄此值
內存泄漏第三個常見來源是監聽器和其餘回調,若是客戶端在你實現的 API 中註冊回調,卻沒有顯示的取消,那麼就會積聚。須要確保回調當即被看成垃圾回收的最佳方法是隻保存他的弱引用,例如將他們保存成爲 WeakHashMap 中的鍵。
1.儘可能減小使用靜態變量,類的靜態變量的生命週期和類同步的。
2.聲明對象引用以前,明確內存對象的有效做用域,儘可能減少對象的做用域,將類的成員變量改寫爲方法內的局部變量;
3.減小長生命週期的對象持有短生命週期的引用;
4.使用StringBuilder和StringBuffer進行字符串鏈接,Sting和StringBuilder以及StringBuffer等均可以表明字符串,其中String字符串表明的是不可變的字符串,後二者表示可變的字符串。若是使用多個String對象進行字符串鏈接運算,在運行時可能產生大量臨時字符串,這些字符串會保存在內存中從而致使程序性能降低。
5.對於不須要使用的對象手動設置null值,無論GC什麼時候會開始清理,咱們都應及時的將無用的對象標記爲可被清理的對象;
6.各類鏈接(數據庫鏈接,網絡鏈接,IO鏈接)操做,務必顯示調用close關閉。