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 的內存洩露表現為一個內存對象的生命周期超出了程序需要它的時間長度。我們有時也將其稱為“對象游離”。
例如:
- public class FileSearch{
- private byte [] content;
- private File mFile;
- public FileSearch(File file){
- mFile = file;
- }
- public boolean hasString(String str){
- int size = getFileSize(mFile);
- content = new byte [size];
- loadFile(mFile, content);
- String s = new String(content);
- return s.contains(str);
- }
- }
在這段代碼中,FileSearch 類中有一個函數 hasString ,用來判斷文檔中是否含有指定的字符串。流程是先將mFile 加載到內存中,然後進行判斷。但是,這裡的問題是,將 content 聲明為了實例變量,而不是本地變量。於是,在此函數返回之後,內存中仍然存在整個文件的數據。而很明顯,這些數據我們後續是不再需要的,這就造成了內存的無故浪費。
要避免這種情況下的內存洩露,要求我們以C/C++ 的內存管理思維來管理自己分配的內存。第一,是在聲明對象引用之前,明確內存對象的有效作用域。在一個函數內有效的內存對象,應該聲明為 local 變量,與類實例生命周期相同的要聲明為實例變量……以此類推。第二,在內存對象不再需要時,記得手動將其引用置空。
復雜數據結構中的內存洩露問題
在實際的項目中,我們經常用到一些較為復雜的數據結構用於緩存程序運行過程中需要的數據信息。有時,由於數據結構過於復雜,或者我們存在一些特殊的需求(例如,在內存允許的情況下,盡可能多的緩存信息來提高程序的運行速度等情況),我們很難對數據結構中數據的生命周期作出明確的界定。這個時候,我們可以使用Java 中一種特殊的機制來達到防止內存洩露的目的。
之前我們介紹過,Java 的 GC 機制是建立在跟蹤內存的引用機制上的。而在此之前,我們所使用的引用都只是定義一個“ Object o; ”這樣形式的。事實上,這只是 Java 引用機制中的一種默認情況,除此之外,還有其他的一些引用方式。通過使用這些特殊的引用機制,配合 GC 機制,就可以達到一些我們需要的效果。