阿里大佬講解Java記憶體溢位示例(堆溢位、棧溢位)

Java架構沒有996發表於2020-12-12

Java記憶體溢位示例(堆溢位、棧溢位)

堆溢位:

/**

* @author LXA

* 堆溢位

*/

public class Heap

{

    public static void main(String[] args)

    {

        ArrayList list=new ArrayList();

        while(true)

        {

            list.add(new Heap());

        }

    }

}

報錯:

java.lang.OutOfMemoryError: Java heap space

棧溢位:

/**

* @author LXA

* 棧溢位

*/

public class Stack

{

    public static void main(String[] args)

    {

        new Stack().test();

    }

    public void test()

    {

        test();

    }

}

報錯:

java.lang.StackOverflowError

Java記憶體管理機制

在C++ 語言中,如果需要動態分配一塊記憶體,程式設計師需要負責這塊記憶體的整個生命週期。從申請分配、到使用、再到最後的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程式設計師很容易由於疏忽而忘記釋放記憶體,從而導致記憶體的洩露。 Java 語言對記憶體管理做了自己的優化,這就是垃圾回收機制。 Java 的幾乎所有記憶體物件都是在堆記憶體上分配(基本資料型別除外),然後由 GC ( garbage collection)負責自動回收不再使用的記憶體。

上面是Java 記憶體管理機制的基本情況。

但是如果僅僅理解到這裡,我們在實際的專案開發中仍然會遇到記憶體洩漏的問題。也許有人表示懷疑,既然 Java 的垃圾回收機制能夠自動的回收記憶體,怎麼還會出現記憶體洩漏的情況呢?這個問題,我們需要知道 GC 在什麼時候回收記憶體物件,什麼樣的記憶體物件會被 GC 認為是“不再使用”的。

Java中對記憶體物件的訪問,使用的是引用的方式。

在 Java 程式碼中我們維護一個記憶體物件的引用變數,通過這個引用變數的值,我們可以訪問到對應的記憶體地址中的記憶體物件空間。在 Java 程式中,這個引用變數本身既可以存放堆記憶體中,又可以放在程式碼棧的記憶體中(與基本資料型別相同)。
GC 執行緒會從程式碼棧中的引用變數開始跟蹤,從而判定哪些記憶體是正在使用的。


如果 GC 執行緒通過這種方式,無法跟蹤到某一塊堆記憶體,那麼 GC 就認為這塊記憶體將不再使用了(因為程式碼中已經無法訪問這塊記憶體了)。

通過這種有向圖的記憶體管理方式,當一個記憶體物件失去了所有的引用之後,GC 就可以將其回收。反過來說,如果這個物件還存在引用,那麼它將不會被 GC 回收,哪怕是 Java 虛擬機器丟擲 OutOfMemoryError 。

Java記憶體洩露

  • 一般來說記憶體洩漏有兩種情況。一種情況如在C/C++
    語言中的,在堆中的分配的記憶體,在沒有將其釋放掉的時候,就將所有能訪問這塊記憶體的方式都刪掉(如指標重新賦值);
  • 另一種情況則是在記憶體物件明明已經不需要的時候,還仍然保留著這塊記憶體和它的訪問方式(引用)。第一種情況,在 Java
    中已經由於垃圾回收機制的引入,得到了很好的解決。所以, Java 中的記憶體洩漏,主要指的是第二種情況。
Vector v = new  Vector( 10 );  
 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 程式發生了記憶體洩漏。


儘管對於C/C++ 中的記憶體洩露情況來說, Java 記憶體洩露導致的破壞性小,除了少數情況會出現程式崩潰的情況外,大多數情況下程式仍然能正常執行。但是,在移動裝置對於記憶體和 CPU都有較嚴格的限制的情況下, Java 的記憶體溢位會導致程式效率低下、佔用大量不需要的記憶體等問題。這將導致整個機器效能變差,嚴重的也會引起丟擲 OutOfMemoryError ,導致程式崩潰。


一般情況下記憶體洩漏的避免

在不涉及複雜資料結構的一般情況下,Java 的記憶體洩露表現為一個記憶體物件的生命週期超出了程式需要它的時間長度。我們有時也將其稱為“物件遊離”。


在這段程式碼中,FileSearch 類中有一個函式 hasString ,用來判斷文件中是否含有指定的字串。流程是先將mFile 載入到記憶體中,然後進行判斷。但是,這裡的問題是,將 content 宣告為了例項變數,而不是本地變數。

於是,在此函式返回之後,記憶體中仍然存在整個檔案的資料。而很明顯,這些資料我們後續是不再需要的,這就造成了記憶體的無故浪費。


要避免這種情況下的記憶體洩露,要求我們以C/C++ 的記憶體管理思維來管理自己分配的記憶體。第一,是在宣告物件引用之前,明確記憶體物件的有效作用域。在一個函式內有效的記憶體物件,應該宣告為 local 變數,與類例項生命週期相同的要宣告為例項變數……以此類推。第二,在記憶體物件不再需要時,記得手動將其引用置空。

Java中的幾種引用方式

Java中有幾種不同的引用方式,它們分別是:強引用、軟引用、弱引用和虛引用。下面,我們首先詳細地瞭解下這幾種引用方式的意義。

強引用

在此之前我們介紹的內容中所使用的引用 都是強引用,這是使用最普遍的引用。如果一個物件具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空 間不足,Java 虛擬機器寧願丟擲 OutOfMemoryError 錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。

軟引用(SoftReference )

SoftReference 類的一個典型用途就是用於記憶體敏感的快取記憶體。 SoftReference 的原理是:在保持對物件的引用時保證在 JVM 報告記憶體不足情況之前將清除所有的軟引用。關鍵之處在於,垃圾收集器在執行時可能會(也可能不會)釋放軟可及物件。物件是否被釋放取決於垃圾收集器的演算法 以及垃圾收集器執行時可用的記憶體數量。

弱引用(WeakReference )

WeakReference 類的一個典型用途就是規範化對映( canonicalized mapping )。另外,對於那些生存期相對較長而且重新建立的開銷也不高的物件來說,弱引用也比較有用。關鍵之處在於,垃圾收集器執行時如果碰到了弱可及物件,將釋放 WeakReference 引用的物件。然而,請注意,垃圾收集器可能要執行多次才能找到並釋放弱可及物件。

虛引用(PhantomReference )

PhantomReference 類只能用於跟蹤對被引用物件即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。 PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。

當垃圾收集器確定了某個物件是虛可及物件時, PhantomReference 物件就被放在它的 ReferenceQueue 上。將 PhantomReference 物件放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 物件引用的物件已經結束,可供收集了。這使您能夠剛好在物件佔用的記憶體被回收之前採取行動。 Reference與 ReferenceQueue 的配合使用。

最新2020整理收集的一些高頻面試題(都整理成文件),有很多幹貨,包含mysql,netty,spring,執行緒,spring cloud、jvm、原始碼、演算法等詳細講解,也有詳細的學習規劃圖,面試題整理等,
需要獲取這些內容的朋友請加Q君樣:
756584822

相關文章