程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java的垃圾回收

Java的垃圾回收

編輯:關於JAVA

1. 垃圾回收的意義

在C++中,對象所占的內存在程序結束運行之前一直被占用,在明確釋放之前不能分 配給其它對象;而在Java中,當沒有對象引用指向原先分配給某個對象的內存時,該內存便成為垃圾。JVM的 一個系統級線程會自動釋放該內存塊。垃圾回收意味著程序不再需要的對象是"無用信息",這些信 息將被丟棄。當一個對象不再被引用的時候,內存回收它占領的空間,以便空間被後來的新對象使用。事實上 ,除了釋放沒用的對象,垃圾回收也可以清除內存記錄碎片。由於創建對象和垃圾回收器釋放丟棄對象所占的 內存空間,內存會出現碎片。碎片是分配給對象的內存塊之間的空閒內存洞。碎片整理將所占用的堆內存移到 堆的一端,JVM將整理出的內存分配給新的對象。

垃圾回收能自動釋放內存空間,減輕編程的負擔。這使 Java 虛擬機具有一些優點。首先,它能使編程效率提高。在沒有垃圾回收機制的時候,可能要花許多時間來 解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾回收機制可大大縮短時間。其次是它保護程序 的完整性, 垃圾回收是Java語言安全性策略的一個重要部份。

垃圾回收的一個潛在的缺點是它的開銷影響 程序性能。Java虛擬機必須追蹤運行程序中有用的對象,而且最終釋放沒用的對象。這一個過程需要花費處理 器的時間。其次垃圾回收算法的不完備性,早先采用的某些垃圾回收算法就不能保證100%收集到所有的廢棄內 存。當然隨著垃圾回收算法的不斷改進以及軟硬件運行效率的不斷提升,這些問題都可以迎刃而解。
2. 垃圾收集的算法分析

Java語言規范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾回收算法 一般要做2件基本的事情:(1)發現無用信息對象;(2)回收被無用對象占用的內存空間,使該空間可被程 序再次使用。

大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就是正在執行的Java程序可 以訪問的引用變量的集合(包括局部變量、參數、類變量),程序可以使用引用變量訪問對象的屬性和調用對象 的方法。垃圾回收首先需要確定從根開始哪些是可達的和哪些是不可達的,從根集可達的對象都是活動對象, 它們不能作為垃圾被回收,這也包括從根集間接可達的對象。而根集通過任意路徑不可達的對象符合垃圾收集 的條件,應該被回收。下面介紹幾個常用的算法。

2.1. 引用計數法(Reference Counting Collector)

引用計數法是唯一沒有使用根集的垃圾回收的法,該算法使用引用計數器來區分存活對象和不再使用的對象。 一般來說,堆中的每個對象對應一個引用計數器。當每一次創建一個對象並賦給一個變量時,引用計數器置為 1。當對象被賦給任意變量時,引用計數器每次加1當對象出了作用域後(該對象丟棄不再使用),引用計數器減 1,一旦引用計數器為0,對象就滿足了垃圾收集的條件。

基於引用計數器的垃圾收集器運行較快,不會長 時間中斷程序執行,適宜地必須實時運行的程序。但引用計數器增加了程序執行的開銷,因為每次對象賦給新 的變量,計數器加1,而每次現有對象出了作用域生,計數器減1。

2.2. tracing算法(Tracing Collector)

tracing算法是為了解決引用計數法的問題而提出,它使用了根集的概念。基於tracing算法的 垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每 個可達對象設置一個或多個位。在掃描識別過程中,基於tracing算法的垃圾收集也稱為標記和清除(mark- and-sweep)垃圾收集器.

2.3. compacting算法(Compacting Collector)

為了解決堆碎片問題,基於 tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的 另一端就變成了一個相鄰的空閒內存區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在 新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,一般增加句柄和句柄表。

2.4. copying算法(Coping Collector)

該算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時 把堆分成一個對象區和多個空閒區,程序從對象區為對象分配空間,當對象滿了,基於coping算法的垃圾回收 就從根集中掃描活動對象,並將每個活動對象復制到空閒區(使得活動對象所占的內存之間沒有空閒間隔),這 樣空閒區變成了對象區,原來的對象區變成了空閒區,程序會在新的對象區中分配內存。

一種典型的基於 coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象區和空閒區域區,在對象區與空閒區域的切換 過程中,程序暫停執行。

2.5. generation算法(Generational Collector)

stop-and-copy垃圾收集器的 一個缺陷是收集器必須復制所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設 計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩 個或多個,每個子堆作為對象的一代 (generation)。由於多數對象存在的時間比較短,隨著程序丟棄不使用 的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的 對象移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。

2.6. adaptive算法 (Adaptive Collector)

在特定的情況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收 集器就是監控當前堆的使用情況,並將選擇適當算法的垃圾收集器。

3. System.gc()方法

命令 行參數透視垃圾收集器的運行

使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法,都可以請求 Java的垃圾回收。在命令行中有一個參數-verbosegc可以查看Java使用的堆內存的情況,它的格式如下:

java -verbosegc classfile

可以看個例子:

class TestGC  
{  
    public static void main(String[] args)  
    {  
      new TestGC();  
      System.gc();  
      System.runFinalization();  
   }  
}

在這個例子中,一個新的對象被創建,由於它沒有使用,所以該對 象迅速地變為不可達,程序編譯後,執行命令: java -verbosegc TestGC 後結果為:

[Full GC 168K- >97K(1984K), 0.0253873 secs]

機器的環境為,Windows 2000 + JDK1.3.1,箭頭前後的數據168K和 97K分別表示垃圾收集GC前後所有存活對象使用的內存容量,說明有168K-97K=71K的對象容量被回收,括號內 的數據1984K為堆內存的總容量,收集所需要的時間是0.0253873秒(這個時間在每次執行的時候會有所不同) 。

需要注意的是,調用System.gc()也僅僅是一個請求(建議)。JVM接受這個消息後,並不是立即做垃 圾回收,而只是對幾個垃圾回收算法做了加權,使垃圾回收操作容易發生,或提早發生,或回收較多而已。

4. finalize()方法

在JVM垃圾回收器收集一個對象之前,一般要求程序調用適當的方法釋放資 源,但在沒有明確釋放資源的情況下,Java提供了缺省機制來終止該對象心釋放資源,這個方法就是finalize ()。它的原型為:

protected void finalize() throws Throwable

在finalize()方法返回之後,對象 消失,垃圾收集開始執行。原型中的throws Throwable表示它可以拋出任何類型的異常。

之所以要使用 finalize(),是存在著垃圾回收器不能處理的特殊情況。假定你的對象(並非使用new方法)獲得了一塊“特 殊”的內存區域,由於垃圾回收器只知道那些顯示地經由new分配的內存空間,所以它不知道該如何釋放這塊 “特殊”的內存區域,那麼這個時候java允許在類中定義一個由finalize()方法。

特殊的區域例如:1 )由於在分配內存的時候可能采用了類似 C語言的做法,而非JAVA的通常new做法。這種情況主要發生在 native method中,比如native method調用了C/C++方法malloc()函數系列來分配存儲空間,但是除非調用 free()函數,否則這些內存空間將不會得到釋放,那麼這個時候就可能造成內存洩漏。但是由於free()方法是 在C/C++中的函數,所以finalize()中可以用本地方法來調用它。以釋放這些“特殊”的內存空間。2)又或者 打開的文件資源,這些資源不屬於垃圾回收器的回收范圍。

換言之,finalize()的主要用途是釋放一些其 他做法開辟的內存空間,以及做一些清理工作。因為在JAVA中並沒有提夠像“析構”函數或者類似概念的函數 ,要做一些類似清理工作的時候,必須自己動手創建一個執行清理工作的普通方法,也就是override Object 這個類中的finalize()方法。例如,假設某一個對象在創建過程中會將自己繪制到屏幕上,如果不是明確地從 屏幕上將其擦出,它可能永遠都不會被清理。如果在finalize()加入某一種擦除功能,當GC工作時, finalize()得到了調用,圖像就會被擦除。要是GC沒有發生,那麼這個圖像就會被一直保存下來。

一旦垃圾回收器准備好釋放對象占用的存儲空間,首先會去調用finalize()方法進行一些必要的清理 工作。只有到下一次再進行垃圾回收動作的時候,才會真正釋放這個對象所占用的內存空間。

在普通的清 除工作中,為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這與C++" 析構函數"的概念稍有抵觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都" 應該"破壞。若將C++對象創建成一個本地對象,比如在堆棧中創建(在Java中是不可能的,Java都在堆 中),那麼清除或破壞工作就會在"結束花括號"所代表的、創建這個對象的作用域的末尾進行。若 對象是用new創建的(類似於Java),那麼當程序員調用C++的 delete命令時(Java沒有這個命令),就會調 用相應的析構函數。若程序員忘記了,那麼永遠不會調用析構函數,我們最終得到的將是一個內存"漏洞 ",另外還包括對象的其他部分永遠不會得到清除。

相反,Java不允許我們創建本地(局部)對象-- 無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,因為垃圾回收器會幫助我們 自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾回收機制,所以Java沒有析 構函數。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對析構函數的需要,或者說 不能消除對析構函數代表的那種機制的需要(原因見下一段。另外finalize()函數是在垃圾回收器准備釋放對 象占用的存儲空間的時候被調用的,絕對不能直接調用finalize(),所以應盡量避免用它)。若希望執行除釋 放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價於C++的析構函數,只 是沒後者方便。

在C++中所有的對象運用delete()一定會被銷毀,而JAVA裡的對象並非總會被垃圾回收器回 收。In another word, 1 對象可能不被垃圾回收,2 垃圾回收並不等於“析構”,3 垃圾回收只與內存有關 。也就是說,並不是如果一個對象不再被使用,是不是要在finalize()中釋放這個對象中含有的其它對象呢? 不是的。因為無論對象是如何創建的,垃圾回收器都會負責釋放那些對象占有的內存。

5. 觸發主GC(Garbage Collector)的條件

JVM進行次GC的頻率很高,但因為這種GC占用時間極短, 所以對系統產生的影響不大。更值得關注的是主GC的觸發條件,因為它對系統影響很明顯。總的來說,有兩個條 件會觸發主GC:

1)當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。因為GC在優先級最低的線 程中進行,所以當應用忙時,GC線程就不會被調用,但以下條件除外。

2)Java堆內存不足時,GC會被調用 。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收 內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法 滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。

由於是否進行主GC由JVM根據系統 環境決定,而系統環境在不斷的變化當中,所以主GC的運行具有不確定性,無法預計它何時必然出現,但可以確定 的是對一個長期運行的應用來說,其主GC是反復進行的。

6. 減少GC開銷的措施

根據上述GC的機 制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現 內存駐留等一系列負面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。 具體措施包括以下幾個方面:

(1)不要顯式調用System.gc()

此函數建議JVM進行主GC,雖然只是 建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。

(2) 盡量減少臨時對象的使用

臨時對象在跳出函數調用後,會成為垃圾,少用臨時變量就相當於減少了垃圾 的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。

(3)對象不用時最好顯式 置為Null

一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利於GC收 集器判定垃圾,從而提高了GC的效率。

(4)盡量使用StringBuffer,而不用String來累加字符串

由於String是固定長的字符串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的 String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因為對次作“+”操作時 都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情 況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間 對象。

(5)能用基本類型如Int,Long,就不用Integer,Long對象

基本類型變量占用的內存資源比 相應對象占用的少得多,如果沒有必要,最好使用基本變量。

(6)盡量少用靜態對象變量

靜態變 量屬於全局變量,不會被GC回收,它們會一直占用內存。

(7)分散對象創建或刪除的時間

集中在 短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回 收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾 對象,空閒空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會。

下面這個例子向大家 展示了垃圾收集所經歷的過程,並對前面的陳述進行了總結。

class Chair {  
  static boolean gcrun = false;  
  static boolean f = false;  
  static int created = 0;  
  static int finalized = 0;  
  int i;  
  Chair() {  
      i = ++created;  
      if(created == 47)  
         System.out.println("Created 47");  
  }  
  protected void finalize() {  
      if(!gcrun) {  
         gcrun = true;  
         System.out.println("Beginning to finalize after " + created + " Chairs have been 

created");  
      }  
      if(i == 47) {  
         System.out.println("Finalizing Chair #47, " +"Setting flag to stop Chair 

creation");  
         f = true;  
      }  
      finalized++;  
      if(finalized >= created)  
         System.out.println("All " + finalized + " finalized");  
  }  
}  
      
public class Garbage {  
  public static void main(String[] args) {  
  if(args.length == 0) {  
     System.err.println("Usage: /n" + "java Garbage before/n or:/n" + "java Garbage after");  
     return;  
  }  
  while(!Chair.f) {  
     new Chair();  
     new String("To take up space");  
  }  
  System.out.println("After all Chairs have been created:/n" + "total created = " + 

Chair.created +  
  ", total finalized = " + Chair.finalized);  
  if(args[0].equals("before")) {  
    System.out.println("gc():");  
    System.gc();  
    System.out.println("runFinalization():");  
    System.runFinalization();  
  }  
  System.out.println("bye!");  
  if(args[0].equals("after"))  
     System.runFinalizersOnExit(true);  
  }  
}

上面這個程序創建了許多Chair對象,而且在垃圾收集器開始運行後 的某些時候,程序會停止創建Chair。由於垃圾收集器可能在任何時間運行,所以我們不能准確知道它在何時 啟動。因此,程序用一個名為gcrun的標記來指出垃圾收集器是否已經開始運行。利用第二個標記f,Chair可 告訴main()它應停止對象的生成。這兩個標記都是在finalize()內部設置的,它調用於垃圾收集期間。另兩個 static變量--created以及 finalized--分別用於跟蹤已創建的對象數量以及垃圾收集器已進行完收尾工作的 對象數量。最後,每個Chair都有它自己的(非 static)int i,所以能跟蹤了解它具體的編號是多少。編號 為47的Chair進行完收尾工作後,標記會設為true,最終結束Chair對象的創建過程。

7. 關於垃圾回收 的幾點補充

經過上述的說明,可以發現垃圾回收有以下的幾個特點:

(1)垃圾收集發生的不可預知性 :由於實現了不同的垃圾回收算法和采用了不同的收集機制,所以它有可能是定時發生,有可能是當出現系統 空閒CPU資源時發生,也有可能是和原始的垃圾收集一樣,等到內存消耗出現極限時發生,這與垃圾收集器的 選擇和具體的設置都有關系。

(2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標 記活著的對象;(b)垃圾收集器能夠精確地定位對象之間的引用關系。前者是完全地回收所有廢棄對象的前 提,否則就可能造成內存洩漏。而後者則是實現歸並和復制等算法的必要條件。所有不可達對象都能夠可靠地 得到回收,所有對象都能夠重新分配,允許對象的復制和對象內存的縮並,這樣就有效地防止內存的支離破碎 。

(3)現在有許多種不同的垃圾收集器,每種有其算法且其表現各異,既有當垃圾收集開始時就停止應用 程序的運行,又有當垃圾收集開始時也允許應用程序的線程運行,還有在同一時間垃圾收集多線程運行。

(4)垃圾收集的實現和具體的JVM 以及JVM的內存模型有非常緊密的關系。不同的JVM 可能采用不同的垃圾收 集,而JVM 的內存模型決定著該JVM可以采用哪些類型垃圾收集。現在,HotSpot 系列JVM中的內存系統都采用 先進的面向對象的框架設計,這使得該系列JVM都可以采用最先進的垃圾收集。

(5)隨著技術的發展,現 代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設置不同的參數,這就使得 根據不同的應用環境獲得最優的應用性能成為可能。

針對以上特點,我們在使用的時候要注意:

(1) 不要試圖去假定垃圾收集發生的時間,這一切都是未知的。比如,方法中的一個臨時對象在方法調用完畢後就 變成了無用對象,這個時候它的內存就可以被釋放。

(2)Java中提供了一些和垃圾收集打交道的類,而且 提供了一種強行執行垃圾收集的方法--調用System.gc(),但這同樣是個不確定的方法。Java 中並不保證每次 調用該方法就一定能夠啟動垃圾收集,它只不過會向JVM發出這樣一個申請,到底是否真正執行垃圾收集,一 切都是個未知數。

(3)挑選適合自己的垃圾收集器。一般來說,如果系統沒有特殊和苛刻的性能要求,可 以采用JVM的缺省選項。否則可以考慮使用有針對性的垃圾收集器,比如增量收集器就比較適合實時性要求較 高的系統之中。系統具有較高的配置,有比較多的閒置資源,可以考慮使用並行標記/清除收集器。

(4) 關鍵的也是難把握的問題是內存洩漏。良好的編程習慣和嚴謹的編程態度永遠是最重要的,不要讓自己的一個 小錯誤導致內存出現大漏洞。

(5)盡早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是 讓引用變量在退出活動域(scope)後,自動設置為null,暗示垃圾收集器來收集該對象,還必須注意該引用的 對象是否被監聽,如果有,則要去掉監聽器,然後再賦空值。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved