面試官:小夥子,你給我說一下Java中什麼情況會導致記憶體洩漏呢?

前程有光發表於2020-08-06

概念

記憶體洩露:指程式中動態分配記憶體給一些臨時物件,但物件不會被GC回收,它始終佔用記憶體,被分配的物件可達但已無用。即無用物件持續佔有記憶體或無用物件的記憶體得不到及時釋放,從而造成的記憶體空間浪費。

可達性分析演算法

JVM使用可達性分析演算法判斷物件是否存活。

GC Root

通過一系列名為“GC Roots”的物件作為起點,從這些結點開始向下搜尋,搜尋所走過的路徑稱為“引用鏈(Reference Chain)”,當一個物件到GC Roots沒有任何飲用鏈相連時,則證明此物件是不可用的。

object4、object5、object6雖然有互相判斷,但是它們到GC Rootd是不可達的,所以它們將會判定為是可回收物件。

可以作為GC Roots的物件有:

  • 虛擬機器棧(棧幀中的本地變數表)中的引用的物件;
  • 方法區中的類靜態屬性引用的物件;
  • 方法區中的常量引用的物件;
  • 本地方法棧中JNI的引用的物件

雖然Java有垃圾收集器幫組實現記憶體自動管理,雖然GC有效的處理了大部分記憶體,但是並不能完全保證記憶體的不洩漏。

記憶體洩漏

記憶體洩漏就是堆記憶體中不再使用的物件無法被垃圾收集器清除掉,因此它們會不必要地存在。這樣就導致了記憶體消耗,降低了系統的效能,最終導致OOM使得程式終止。

記憶體洩漏的表現:

  • 應用程式長時間連續執行時效能嚴重下降;
  • 應用程式中的OutOfMemoryError堆錯誤;
  • 自發且奇怪的應用程式崩潰;
  • 應用程式偶爾會耗盡連線物件;

可能導致記憶體洩漏的原因:

1. static欄位引起的記憶體洩漏

大量使用static欄位會潛在的導致記憶體洩漏,在Java中,靜態欄位通常擁有與整個應用程式相匹配的生命週期。

解決辦法:最大限度的減少靜態變數的使用;單例模式時,依賴於延遲載入物件而不是立即載入的方式(即採用懶漢模式,而不是餓漢模式)

2. 未關閉的資源導致記憶體洩漏

每當建立連線或者開啟流時,JVM都會為這些資源分配記憶體。如果沒有關閉連線,會導致持續佔有記憶體。在任意情況下,資源留下的開放連線都會消耗記憶體,如果不處理,就會降低效能,甚至OOM。

解決辦法:使用finally塊關閉資源;關閉資源的程式碼,不應該有異常;JDK1.7之後,可以使用太try-with-resource塊。

3. 不正確的equals()和hashCode()

在HashMap和HashSet這種集合中,常常用到equal()和hashCode()來比較物件,如果重寫不合理,將會成為潛在的記憶體洩漏問題。

解決辦法:用最佳的方式重寫equals()和hashCode().

4. 引用了外部類的內部類

非靜態內部類的初始化,總是需要外部類的例項;預設情況下,每個非靜態內部類都包含對其外部類的隱式引用,如果我們在應用程式中使用這個內部類物件,那麼即使在我們的外部類物件超出範圍後,它也不會被垃圾收集器清除掉。

解決辦法:如果內部類不需要訪問外部類包含的類成員,可以轉換為靜態類。

5. finalize方法導致的記憶體洩漏

重寫finalize()方法時,該類的物件不會立即被垃圾收集器收集,如果finalize()方法的程式碼有問題,那麼會潛在的印發OOM;

解決辦法:避免重寫finalize()方法。

6. 常量字串造成的記憶體洩漏

如果我們讀取一個很大的String物件,並呼叫了intern(),那麼它將放到字串池中,位於PermGen中,只要應用程式執行,該字串就會保留,這就會佔用記憶體,可能造成OOM。(針對JDK1.6及以前,常量池在PermGen永久代中)

解決辦法:增加PermGen的大小,-XX:MaxPermSize=512M;JDK1.7以後字串池轉移到了堆中。

intern()方法詳解:

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = str3.intern();

System.out.println(str1 == str2);
System.out.println(str2 == str3);

System.out.println(str1 == str4);
System.out.println(str3 == str4);

true, false, true, false

intern()方法搜尋字串常量池,如果存在指定的字串,就返回之;

否則,就將該字串放入常量池並返回之。

換言之,intern()方法保證每次返回的都是 同一個字串物件

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abcd");
String str4 = str3.intern();
String str5 = "abcd";

System.out.println(str1 == str2);
System.out.println(str2 == str3);

System.out.println(str1 == str4);
System.out.println(str3 == str4);

System.out.println(str4 == str5);

true
false
false
false
true

為何要使用intern()方法?看看equals方法的原始碼:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

可以看到,比較兩個字串的時候,首先比較兩個字串物件是否地址相同,不同再挨個比較字元。這樣就大大加快了比較的速度。否則若每次都挨個比較將是非常耗時的。

7. 使用ThreadLocal造成記憶體洩漏

使用ThreadLocal時,每個執行緒只要處於存活狀態就可保留對其ThreadLocal變數副本的隱式呼叫,且將保留其自己的副本。使用不當,就會引起記憶體洩漏。

一旦執行緒不再存在,該執行緒的threadLocal物件就應該被垃圾收集,而現線上程的建立都是使用執行緒池,執行緒池有執行緒重用的功能,因此執行緒就不會被垃圾回收器回收。所以使用到ThreadLocal來保留執行緒池中的執行緒的變數副本時,ThreadLocal沒有顯式地刪除時,就會一直保留在記憶體中,不會被垃圾回收。

解決辦法:不再使用ThreadLocal時,呼叫remove()方法,該方法刪除了此變數的當前執行緒值。不要使用ThreadLocal.set(null),它只是查詢與當前執行緒關聯的Map並將鍵值中這個threadLocal物件所對應的值為null,並沒有清除這個鍵值對。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!

相關文章