Java 內存管理原理、內存泄漏實例及解決方案研究 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
}
}
二、基於數組的集合
當數組用於實現諸如堆棧或環形緩衝區之類的數據結構時,會出現另外一種形式的對象遊離。清單 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 管理它本身的內存,所以負責確保對再也不須要的對象的引用被清除。使用數組來實現堆棧或緩衝區是一種優化,能夠減小分配,可是會給實現者帶來更大的負擔,須要仔細地管理存儲在數組中的引用的生命期。