Java內存回收機制

一、Java對象在內存引用狀態

內存泄露:程序運行過程中,會不斷分配內存空間,那些不再使用的內存空間應該即時回收它們,從而保證系統可以再次使用這些內存,如果存在無用的內存沒有被回收回來,這就是內存泄漏.
(1)強引用
  這是java程序中最常見的引用方式,程序創建一個對象,並把這個對象賦給一個引用變量,這個引用變量就是強引用.java程序可通過強引用來訪問實際的對象。當一個對象被一個或一個以上的強引用變量引用時,它處於可達狀態,它不可能被系統垃圾回收機制回收。
  強引用是Java編程中廣泛使用的引用類型,被強引用所引用的Java對象絕不會被垃圾回收機制回收,即使系統內存緊張;即使有些Java對象以後永遠也不會被用到,JVM也不會回收被強引用所引用的Java對象.
  由於JVM肯定不會回收強引用所引用的JAVA對象,因此強引用是造成JAVA內存泄漏的主要原因。
    如 ReceiptBean rb=new ReceiptBean(); rb就代表了一種強引用的方式
(2)軟引用
  軟引用需要通過SoftReference類來實現,當一個對象只具有軟引用時,它可能被垃圾回收機制回收。對於只有軟引用的對象而言,當系統內存空間足夠時,它不會被系統回收,程序也可以使用該對象;當系統內存空間不足時,系統將回收它.
  軟引用通常用在對內存敏感的程序中,軟引用是強引用很好的替代。對於軟引用,當系統內存空間充足時,軟引用與強引用沒有太大的區別,當系統內存空間不足時,被軟引用所引用的JAVA對象可以被垃圾回收機制回收,從而避免系統內存不足的異常.
  當程序需要大量創建某個類的新對象,而且有可能重新訪問已創建老對象時,可以充分使用軟引用來解決內存緊張的問題。

例如需要訪問1000個Person對象,可以有兩種方式
方法一
 依次創建1000個對象,但只有一個Person引用指向最後一個Person對象
方法二 定義一個長度爲1000個的Person數組,每個數組元素引用一個Person對象.

對於方法一,弱點很明顯,程序不允許需要重新訪問前面創建的Person對象,即使這個對象所佔的空間還沒有被回收。但已經失去了這個對象的引用,因此也不得不重新創建一個新的Person對象(重新分配內存),而那個已有的Person對象(完整的,正確的,可用的)則只能等待垃圾回收
對於方法二,優勢是可以隨時重新訪問前面創建的每個Person對象,但弱點也有,如果系統堆內存空間緊張,而1000個Person對象都被強引用引着,垃圾回收機制也不可能回收它們的堆內存空間,系統性能將變成非常差,甚至因此內存不足導致程序中止。

  如果用軟引用則是一種較好的方案,當堆內存空間足夠時,垃圾回收機制不會回收Person對象,可以隨時重新訪問一個已有的Person對象,這和普通的強引用沒有任何區別。但當heap堆內存空間不足時,系統也可以回收軟引用引用的Person對象,從而提高程序運行性能,避免垃圾回收.

當程序使用強引用時,無論系統堆內存如何緊張,JVM垃圾回收機制都不會回收被強引用所引用的Java對象,因此最後導致程序因內存不足而中止。但如果把強引用改爲軟引用,就完成可以避免這種情況,這就是軟引用的優勢所在.

(3)弱引用
  弱引用與軟引用有點相似,區別在於弱引用所引用對象的生存期更短。弱引用通過WeakReference類實現,弱引用和軟引用很像,但弱引用的引用級別更低。對於只有弱引用的對象而言,當系統垃圾回收機制運行時,不管系統內存是否足夠,總會回收該對象所佔用的內存。當然,並不是說當一個對象只有弱引用時,它就會立即被回收,正如那些失去引用的對象一樣,必須等到系統垃圾回收機制運行時纔會被回收.

總結說明:

1.弱引用具有很大的不確定性,因爲每次垃圾回收機制執行時都會回收弱引用所引用的對象,而垃圾回收機制的運行又不受程序員的控制,因此程序獲取弱引用所引用的java對象時必須小心空指針異常,通過弱引用所獲取的java對象可能是null

2.由於垃圾回收的不確定性,當程序希望從弱引用中取出被引用對象時,可能這個被引用對象已經被釋放了。如果程序需要使用被引用的對象,則必須重新創建該對象。

(4)虛引用
  軟引用和弱引用可以單獨使用,但虛引用不能單獨使用,單獨使用虛引用沒有太大的意義。虛引用的主要作用就是跟蹤對象被垃圾回收的狀態,程序可以通過檢查虛引用關聯的引用隊列中是否包含指定的虛引用,從而瞭解虛引用所引用的對象是否將被回收.
  引用隊列由java.lang.ref.ReferenceQueue類表示,它用於保存被回收對象的引用。當把軟引用,弱引用和引用隊列聯合使用時,系統回收被引用的對象之後,將會把被回收對象對應的引用添加到關聯的引用隊列中。與軟引用和弱引用不同的是,虛引用在對象被釋放之前,將把它對應的虛引用添加到關聯的隊列中,這使得可以在對象被回收之前採取行動。
虛引用通過PhantomReference類實現,它完全類似於沒有引用。虛引用對對象本身沒有大的影響,對象甚至感覺不到虛引用的存在。如果一個對象只有一個虛引用,那它和沒有引用的效果大致相同。虛引用主要用於跟蹤對象被垃圾回收的狀態,虛引用不能單獨使用,虛引用必須和隊列ReferenceQueue聯合使用.

二、Java內存泄露

對於c++程序來說,對象佔用的內存空間都必須由程序顯式回收,如果程序員忘記了回收它們,那它們所佔用的內存空間就會產生內存泄漏;對於java程序來說,所有不可達的對象都由垃圾回收機制負責回收,因此程序員不需要考慮這部分的內存泄漏。但如果程序中有一些java對象,它們處於可達狀態,但程序以後永遠都不會再訪問它們,那它們所佔用的空間也不會被回收,它們所佔用的空間也會產生內存泄漏.

例如:
有ArrayList的長度是4,有四個元素“網”,“絡”,“時”,“空”,當我們刪除了ArrayList中的"網"這個元素時,它的size等於3,也就是該ArrayList認爲自己只有3個元素,因此它永遠也不會去訪問底層數組的第4個元素。對於程序本身來說,這個對象已經變成了垃圾,但對於垃圾回收機制來說,這個對象依然處於可達狀態,因此不會回收它,這就產生了內存泄漏了  

再看下面程序採用基於數組的方式實現了一個Stack,大家可以找找這個程序中的內存泄漏

 


package list;

public class Stack
{
	//存放棧內元素的數組
	private Object[] elementData;
	//記錄棧內元素的個數
	private int size = 0;
	private int capacityIncrement;
	//以指定初始化容量創建一個Stack
	public Stack(int initialCapacity){
		elementData = new Object[initialCapacity];
	}
	public Stack(int initialCapacity , int capacityIncrement){
		this(initialCapacity);
		this.capacityIncrement = capacityIncrement;
	}
	//向“棧”頂壓入一個元素
	public void push(Object object){
		ensureCapacity();
		elementData[size++] = object;
		// if(size==10) System.out.println("size="+size);
	}
	//出棧
	public Object pop(){
		if(size == 0){
			throw new RuntimeException("空棧異常");
		}
		return elementData[--size];
	}
	public int size(){
		return size;
	}
	//保證底層數組能容納棧內所有元素
	private void ensureCapacity(){
		//增加堆棧的容量
		if(elementData.length==size){
			Object[] oldElements = elementData;
			int newLength = 0;
			//已經設置capacityIncrement
			if (capacityIncrement > 0){
				newLength = elementData.length + capacityIncrement;
			}else{
				//將長度擴充到原來的1.5倍
				newLength = (int)(elementData.length * 1.5);
			}
			// System.out.println("newLength="+newLength);
			elementData = new Object[newLength];
			//將原數組的元素複製到新數組中
			System.arraycopy(oldElements , 0 , elementData , 0 , size);
		}
	}
	public static void main(String[] args){
		Stack stack = new Stack(10);
		//向棧頂壓入10個元素
		for (int i = 0 ; i < 10 ; i++){
			stack.push("元素" + i);
		}
		//依次彈出10個元素
		for (int i = 0 ; i < 10 ; i++){
			System.out.println(stack.pop());
		}
	}
}

 

     前面程序實現了一個簡單的Stack,併爲這個Stack實現了push(),pop()兩個方法,其中pop()方法可能產生內存泄漏。爲了說明這個Stack的內存泄漏,程序main方法創建了一個Stack對象,先向該Stack壓入10個元素。注意:此時底層elementData的長度爲10,每人數組元素都引用一個字符串。
  接下來,程序10次調用pop()方法彈出棧頂元素。注意pop()方法產生的內存泄漏,它只做了兩件事:一是修飾Stack的size屬性,也就是記錄棧內元素減1,二是返回elementData數組中索引爲size-1的元素

  也就是說,每調用pop方法一次,Stack會記錄該棧的尺寸減1,但未清除elementData數組的最後一個元素的引用,這樣就會產生內存泄漏。類似地,也應該按ArrayList類的源代碼改寫此處pop()方法的源代碼,如下所示
public Object pop()
{
if(size == 0)
{
throw new RuntimeException("空棧異常");
}
Object obj=elementData[--size];
//清除最後一個數組元素的引用,避免內存泄漏
elementData[size]=null; 
return obj; 
}

三、內存管理的小技巧

  儘可能多的掌握Java的內存回收,垃圾回收機制是爲了更好地管理JVM的內存,這樣才能提高java程序的運行性能。根據前面介紹的內存機制,下面給出java內存管理的幾個小技巧。 
(1)儘量使用直接量 
  當需要使用字符串,還有Byte,Short,Integer,Long,Float,Double,Boolean,Charater包裝類的實例時,程序不應該採用new的方式來創建對象,而應該直接採用直接量來創建它們。 
例如,程序需要"hello"字符串,應該採用如下代碼 
String str="hello"' 
上面這種方式創建一個"hello"字符串,而且JVM的字符串緩存池還會緩存這個字符串。但如果程序採用 
String str=new String("hello"); 
  此時程序同樣創建了一個緩存在字符串緩存池中的"hello"字符串。除此之外,str所引用的String對象底層還包含一個char[]數組,這個char[]數組裏依次存放了h,e,l,l.o等字符串。

(2)使用StringBuffer和StringBuilder進行字符串拼接
  如果程序中採用多個String對象進行字符串連接運算,在運行時將生成大量臨時字符串,這些字符串會保存在內存中從而導致程序性能下降

(3)儘早釋放無用對象的引用
  大部分時候,方法局部引用變量所引用的對象會隨着方法結束而變成垃圾,因爲局部變量的生存週期很短,當方法運行結束之時,該方法內的局部變量就結束了生命週期。因此,大部分時候程序無需將局部引用變量顯式設爲null.
但是下面程序中的情形則需顯式設爲null比較好了

public void info()
{
Object obj=new Objec();
System.out.println(obj.toString());
System.out.println(obj.toString());
obj=null;
//執行耗時,耗內存的操作
//或者調用耗時,耗內存的操作的方法
..
}

  對於上面程序所示的info()方法,如果需要“執行耗時,耗內存的操作”或者"或者調用耗時,耗內存的操作的方法",那麼上面程序中顯式設置obj=null就是有必要的了。可能的情況是:當程序在“執行耗時,耗內存的操作”或者"或者調用耗時,耗內存的操作的方法",obj之前所引用的對象可能被垃圾加收了。

(4)儘量少用靜態變量
  從理論來說,Java對象對象何時被回收由垃圾回收機制決定,對程序員來說是不確定的。由於垃圾回收機制判斷一個對象是否是垃圾的唯一標準就是該對象是否有引用變量引用它,因此要儘早釋放對象的引用。
  最壞的情況是某個對象被static變量所引用,那麼垃圾回收機制通常是不會回收這個對象所佔用的內存的。

Class Person
{
static Object obj=new Object();
}
  對於上面的Object對象而言,只要obj變量還引用它,就會不會被垃圾回收機制所回收
Person類所對應的Class對象會常駐內存,直到程序結束,因此obj所引用的Object對象一旦被創建,也會常駐內存,直到程序運行結束。

(5)避免在經常調用的方法,循環中創建Java對象

public class Test
{
public static void main(String[] args)
{
for(int i=0;i<10;i++)
{
Object obj=new Object();
//執行其它操作...
}
}
}

  上面物循環產生了10個對象,系統要不斷地爲這些對象分配內存空間,執行初始化操作。它們的生存時間並不長,接下來系統又需要回收它們所佔用的內存空間是,這種不斷分配內存,回收操作中,程序的性能受到了很大的影響。

(6)緩存經常使用的對象
  如果有些對象需要經常使用,可以考慮把這些對象用緩存池保存起來,這樣下次需要時就可直接拿出來這些對象來用。典型的緩存池是數據連接池,數據連接池裏緩存了大量的數據庫連接,每次程序需要訪問數據庫時都可直接取出數據庫連接。
  除此之外,如果系統裏還有一些常用的基礎信息,比如信息化信息裏包含的員工信息,物料信息等,也可以考慮對它們進行緩存。
使用緩存通常有兩種方法
1.使用HashMap進行緩存
2.直接使用開源緩存項目。(如OSCache,Ehcahe等)

(7)儘量不要用finalize方法
  在一個對象失去引用之後,垃圾回收器準備回收該對象之前,垃圾回收器會先調用對象的finalize()方法來執行資源清理。出於這種考慮,可能有些開發者會考慮使用finalize()方法來進和清理。
在垃圾回收器本身已經嚴重製約應用程序性能的情況下,如果再選擇使用finalize方法進行資源清理,無疑是一種火上澆油的行爲,這將導致垃圾回收器的負擔更大,導致程序運行效率更低

(8)考慮使用SoftReference軟引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章