Java 內存管理原理、內存泄漏實例及解決方案研究
在項目的最后階段,就是要防止系統的內存泄漏了,順便找了些資料,看了些java內存泄漏的實例及解決,總結一下:
Java是如何管理內存
為了判斷Java中是否有內存泄露,我們首先必須了解Java是如何管理內存的。Java的內存管理就是對象的分配和釋放問題。在Java中,程序員需要 通過關鍵字new為每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由GC決定和執行的。在Java中,內存的分配是由程序完成的,而內存的釋放是有GC完成的,這種收支兩條線 的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是Java程序運行速度較慢的原因之一。因為,GC為了能夠正確釋放對象,GC必須 監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。
監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。
為 了更好理解GC的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一 個圖的起始頂點,例如大多程序從main進程開始執行,那么該圖就是以main進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對 象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被GC回收。
以下,我們舉一個例子說明如何用有向圖表示內存管理。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。
Java 使用有向圖的方式進行內存管理,可以消除引用循環的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那么GC也是可以回收它們的。這種方式的 優點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相比,精度行低 (很難處理循環引用的問題),但執行效率很高。
什么是Java中的內存泄露
下面,我們就可以描述什么是內存泄漏。在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖 中,存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄 漏,這些對象不會被GC所回收,然而它卻占用內存。
在C++中,內存泄漏的范圍更大一些。有些對象被分配了內存空間,然后卻不可達,由于C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。
通過分析,我們得知,對于C++,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。
因此,通過以上分析,我們知道在Java中也有內存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。
對 于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規范 定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。JVM調用GC的策 略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心 這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對于基于Web的實時系統,如網絡游戲等,用戶不希望GC突然中斷應用程序執行而進行垃 圾回收,那么我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
下面給出了一個簡單的內存泄露的例子。在這個例子中,我們循環申請Object對象,并將所申請的對象放入一 個Vector中,如果我們僅僅釋放引用本身,那么Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置為null。Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
Object o=new Object();
v.add(o);
o=null;
}
//此時,所有的Object對象都沒有被釋放,因為變量v引用這些對象。
Java內存泄漏的類型、實例及解決
1.對象游離
一 種形式的內存泄漏有時候叫做對象游離(object loitering),是通過清單 1 中的 LeakyChecksum 類來說明的,清單 1 中有一個 getFileChecksum() 方法用于計算文件內容的校驗和。getFileChecksum() 方法將文件內容讀取到緩沖區中以計算校驗和。一種更加直觀的實現簡單地將緩沖區作為 getFileChecksum() 中的本地變量分配,但是該版本比那樣的版本更加 “聰明”,不是將緩沖區緩存在實例字段中以減少內存 churn。該 “優化”通常不帶來預期的好處;對象分配比很多人期望的更便宜。(還要注意,將緩沖區從本地變量提升到實例變量,使得類若不帶有附加的同步,就不再是線程 安全的了。直觀的實現不需要將 getFileChecksum() 聲明為 synchronized,并且會在同時調用時提供更好的可伸縮性。)
清單 1. 展示 “對象游離” 的類
// BAD CODE - DO NOT EMULATE
public class LeakyChecksum {
private byte[] byteArray;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len];
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
這 個類存在很多的問題,但是我們著重來看內存泄漏。緩存緩沖區的決定很可能是根據這樣的假設得出的,即該類將在一個程序中被調用許多次,因此它應該更加有 效,以重用緩沖區而不是重新分配它。但是結果是,緩沖區永遠不會被釋放,因為它對程序來說總是可及的(除非 LeakyChecksum 對象被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大文件一樣大小的緩沖區。退一萬步說,這也會給垃圾收集器帶來壓力,并且要求更頻繁的收集;為計算未來的校驗和而保持一個大型 緩沖區并不是可用內存的最有效利用。
LeakyChecksum 中問題的原因是,緩沖區對于 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命周期已經被人為延長了,因為將它提升到了實例字段。因此,該類必須自己管理緩沖區的生命周期,而不是讓 JVM 來管理。
軟引用
弱 引用如何可以給應用程序提供當對象被程序使用時另一種到達該對象的方法,但是不會延長對象的生命周期。Reference 的另一個子類 —— 軟引用 —— 可滿足一個不同卻相關的目的。其中弱引用允許應用程序創建不妨礙垃圾收集的引用,軟引用允許應用程序通過將一些對象指定為 “expendable” 而利用垃圾收集器的幫助。盡管垃圾收集器在找出哪些內存在由應用程序使用哪些沒在使用方面做得很好,但是確定可用內存的最適當使用還是取決于應用程序。如 果應用程序做出了不好的決定,使得對象被保持,那么性能會受到影響,因為垃圾收集器必須更加辛勤地工作,以防止應用程序消耗掉所有內存。
高 速緩存是一種常見的性能優化,允許應用程序重用以前的計算結果,而不是重新進行計算。高速緩存是 CPU 利用和內存使用之間的一種折衷,這種折衷理想的平衡狀態取決于有多少內存可用。若高速緩存太少,則所要求的性能優勢無法達到;若太多,則性能會受到影響, 因為太多的內存被用于高速緩存上,導致其他用途沒有足夠的可用內存。因為垃圾收集器比應用程序更適合決定內存需求,所以應該利用垃圾收集器在做這些決定方 面的幫助,這就是件引用所要做的。
如果一個對象惟一剩下的引用是弱引用或軟引用,那么該對象是軟可及的(softly reachable)。垃圾收集器并不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 內存時才收集軟可及的對象。軟引用對于垃圾收集器來說是這樣一種方式,即 “只要內存不太緊張,我就會保留該對象。但是如果內存變得真正緊張了,我就會去收集并處理這個對象。” 垃圾收集器在可以拋出 OutOfMemoryError 之前需要清除所有的軟引用。
通過使用一個軟引用來管理高速緩存的緩沖區,可以解決 LeakyChecksum 中的問題,如清單 2 所示。現在,只要不是特別需要內存,緩沖區就會被保留,但是在需要時,也可被垃圾收集器回收:
清單 2. 用軟引用修復 LeakyChecksum
public class CachingChecksum {
private SoftReferencebufferRef;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
byte[] byteArray = bufferRef.get();
if (byteArray == null || byteArray.length < len) {
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(fileName, byteArray);
// calculate checksum and return it
}
}
2、基于數組的集合
當 數組用于實現諸如堆棧或環形緩沖區之類的數據結構時,會出現另一種形式的對象游離。清單 3 中的 LeakyStack 類展示了用數組實現的堆棧的實現。在 pop() 方法中,在頂部指針遞減之后,elements 仍然會保留對將彈出堆棧的對象的引用。這意味著,該對象的引用對程序來說仍然可及(即使程序實際上不會再使用該引用),這會阻止該對象被垃圾收集,直到該 位置被未來的 push() 重用。
清單 3. 基于數組的集合中的對象游離
public class LeakyStack {
private Object[] elements = new Object[MAX_ELEMENTS];
private int size = 0;
public void push(Object o) { elements[size++] = o; }
public Object pop() {
if (size == 0)
throw new EmptyStackException();
else {
Object result = elements[--size];
// elements[size+1] = null;
return result;
}
}
}
修 復這種情況下的對象游離的方法是,當對象從堆棧彈出之后,就消除它的引用,如清單 3 中注釋掉的行所示。但是這種情況 —— 由類管理其自己的內存 —— 是一種非常少見的情況,即顯式地消除不再需要的對象是一個好主意。大部分時候,認為不應該使用的強行消除引用根本不會帶來性能或內存使用方面的收益,通常 是導致更差的性能或者 NullPointerException。該算法的一個鏈接實現不會存在這個問題。在鏈接實現中,鏈接節點(以及所存儲的對象的引用)的生命期將被自動 與對象存儲在集合中的期間綁定在一起。弱引用可用于解決這個問題 —— 維護弱引用而不是強引用的一個數組 —— 但是在實際中,LeakyStack 管理它自己的內存,因此負責確保對不再需要的對象的引用被清除。使用數組來實現堆棧或緩沖區是一種優化,可以減少分配,但是會給實現者帶來更大的負擔,需 要仔細地管理存儲在數組中的引用的生命期。