Java內存泄露的理解與解決


依賴於引用判斷的內存管理機制

Java中對內存對象的訪問,使用的是引用的方式。
在Java代碼中我們維護一個內存對象的引用變量,通過這個引用變量的值,我們可以訪問到對應的內存地址中的內存對象空間。在Java程序中,這個引用變量本身既可以存放堆內存中,又可以放在代碼棧的內存中(與基本數據類型相同)。GC線程會從代碼棧中的引用變量開始跟蹤,從而判定哪些內存是正在使用的。如果GC線程通過這種方式,無法跟蹤到某一塊堆內存,那么GC就認為這塊內存將不再使用了(即代碼中已經無法訪問這塊內存了)。

Java使用有向圖的方式進行內存管理,可以消除引用循環的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那么GC也是可以回收它們的。

這種方式的優點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。

通過這種有向圖的內存管理方式,當一個內存對象失去了所有的引用之后,GC就可以將其回收。反過來說,如果這個對象還存在引用,那么它將不會被GC回收,哪怕是Java虛擬機拋出OutOfMemoryError。

 

內存泄露的兩種場景

一般來說內存泄漏有兩種情況。一種情況如在C/C++語言中的,在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值),也就是說這塊內存區域不可達;
另一種情況則是在內存對象已經不需要的時候,還仍然保留着這塊內存和它的訪問方式(引用),也就是在有向圖中仍然可達,但是對象已經不再需要。

Java中GC會很好的解決第一種情況,但是第二種情況需要我們在編碼中注意。

可以將對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程序從main進程開始執行,那么該圖就是以main進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被GC回收。

可以這么理解,對於C++,程序員需要自己管理邊和頂點,而對於Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。

Java中的內存泄露

在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;

其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄漏,這些對象不會被GC所回收,然而它卻占用內存。

一般來說是長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄露,盡管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收。

一個典型的內存泄露實例:

Vector v=new Vector(10);
		 for (int i=1;i<100; i++){
			 Object o=new Object();
			 v.add(o);
			 /**
			  * 此時,所有的Object對象都沒有被釋放,因為變量v引用這些對象
			  */
			 o=null;
			 }

 

我們循環申請Object對象,並將所申請的對象放入一個Vector中,如果我們僅僅釋放引用本身,那么Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。

因此,如果對象加入到Vector后,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置為null。

在Java程序中容易發生內存泄露的場景:

1.集合類,集合類僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用
這個集合類如果僅僅是局部變量,根本不會造成內存泄露,在方法棧退出后就沒有引用了會被jvm正常回收,
而如果這個集合類是全局性的變量(比如類中的靜態屬性,全局性的map等即有靜態引用或final一直指向它),那么沒有相應的刪除機制,很可能導致集合所占用的內存只增不減,因此提供這樣的刪除機制或者定期清除策略非常必要。

2.單例模式
不正確使用單例模式是引起內存泄露的一個常見問題,單例對象在被初始化后將在JVM的整個生命周期中存在(以靜態變量的方式),如果單例對象持有外部對象的引用,那么這個外部對象將不能被jvm正常回收,導致內存泄露,考慮下面的例子:

class A{
  public A(){
   B.getInstance().setA(this);
  }
  ….
  }

  

//B類采用單例模式
  class B{
  private A a;
  private static B instance=new B();
  public B(){}
  public static B getInstance(){
  return instance;
  }
  public void setA(A a){
  this.a=a;
  }
  //getter…
  }

顯然B采用singleton模式,他持有一個A對象的引用,而這個A類的對象將不能被回收,想象下如果A是個比較大的對象或者集合類型會發生什么情況。
所以在Java開發過程中和代碼復審的時候要重點關注那些長生命周期對象:全局性的集合、單例模式的使用、類的static變量等等。
在不使用某對象時,顯式地將此對象賦空,遵循誰創建誰釋放的原則,減少內存泄漏發生的機會。

Java中的幾種引用方式

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

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

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

虛引用(PhantomReference)
PhantomReference 類只能用於跟蹤對被引用對象即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。
當垃圾收集器確定了某個對象是虛可及對象時,PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 對象引用的對象已經結束,可供收集了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM