Java記憶體泄漏
Java中的記憶體管理
要了解Java中的記憶體泄漏,首先就得知道Java中的記憶體是如何管理的。
在Java程式中,我們通常使用 new 為對象分配記憶體,而這些記憶體空間都在堆上。
Java判斷對象是否可以回收使用的而是可達性分析演算法。
這個演算法的基本思路就是通過一系列名為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的,下圖對象 object5, object6, object7 雖然有互相判斷,但它們到 GC Roots 是不可達的,所以它們將會判定為是可回收對象。
在 Java 語言中,可作為 GC Roots 對象的包括如下幾種:
- 虛擬機棧(棧幀中的本地變數表)中引用的對象
- 本地方法棧(Native方法)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
什麼是Java中的記憶體泄漏
Java 中的記憶體泄漏,廣義並通俗的說,就是:不再會被使用的對象的記憶體不能被回收,就是記憶體泄漏。
Java 中的記憶體泄漏與 C++ 中的表現有所不同。
在 C++ 中,所有被分配了記憶體的對象,不再使用之後,都必須程式設計師手動的去釋放他們。但是在 Java 中,我們不用自己釋放記憶體,無用的記憶體由 GC 自動清理,這也極大的簡化了我們的編程工作。但實際有時候一些不再會被使用的對象在 GC 看來不能被釋放就會造成記憶體泄漏。
對象都是有生命周期的,有的長,有的短。如果長生命周期的對象持有短生命周期的引用,就很可能會出現記憶體泄漏。例如:
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 回收。
Java中記憶體泄漏的例子
- 靜態集合類
如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 程式發生了記憶體泄漏。
- 各種連接,如資料庫連接、網路連接和IO連接等
在對資料庫進行操作的過程中,首先需要建立與資料庫的連接,當不再使用時,需要調用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);
}
}
- 單例對象在被初始化後將在JVM的整個生命周期中存在(以靜態變數的方式),如果單例對象持有外部對象的引用,那麼這個外部對象將不能被jvm正常回收,導致記憶體泄露
- 快取泄漏
記憶體泄漏的另一個常見來源是快取,一旦你把對象引用放入到快取中,他就很容易遺忘,對於這個問題,可以使用 WeakHashMap 代表快取,此種 Map 的特點是,當除了自身有對 key 的引用外,此 key 沒有其他引用那麼此 map 會自動丟棄此值
- 監聽器和回調
記憶體泄漏第三個常見來源是監聽器和其他回調,如果客戶端在你實現的 API 中註冊回調,卻沒有顯示的取消,那麼就會積聚。需要確保回調立即被當作垃圾回收的最佳方法是只保存他的弱引用,例如將他們保存成為 WeakHashMap 中的鍵。
記憶體泄露解決的原則
1.盡量減少使用靜態變數,類的靜態變數的生命周期和類同步的。
2.聲明對象引用之前,明確記憶體對象的有效作用域,盡量減小對象的作用域,將類的成員變數改寫為方法內的局部變數;
3.減少長生命周期的對象持有短生命周期的引用;
4.使用StringBuilder和StringBuffer進行字元串連接,Sting和StringBuilder以及StringBuffer等都可以代表字元串,其中String字元串代表的是不可變的字元串,後兩者表示可變的字元串。如果使用多個String對象進行字元串連接運算,在運行時可能產生大量臨時字元串,這些字元串會保存在記憶體中從而導致程式性能下降。
5.對於不需要使用的對象手動設置null值,不管GC何時會開始清理,我們都應及時的將無用的對象標記為可被清理的對象;
6.各種連接(資料庫連接,網路連接,IO連接)操作,務必顯示調用close關閉。