熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java的垃圾回收之算法

2022-06-13   來源: Java核心技術 

  引言

  Java的堆是一個運行時數據區類的實例(對象)從中分配空間Java虛擬機(JVM)的堆中儲存著正在運行的應用程序所建立的所有對象這些對象通過newnewarrayanewarray和multianewarray等指令建立但是它們不需要程序代碼來顯式地釋放一般來說堆的是由垃圾回收來負責的盡管JVM規范並不要求特殊的垃圾回收技術甚至根本就不需要垃圾回收但是由於內存的有限性JVM在實現的時候都有一個由垃圾回收所管理的堆垃圾回收是一種動態存儲管理技術它自動地釋放不再被程序引用的對象按照特定的垃圾收集算法來實現資源自動回收的功能

  垃圾收集的意義

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

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

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

  垃圾收集的算法分析

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

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

   引用計數法(Reference Counting Collector)

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

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

  tracing算法(Tracing Collector)

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

  compacting算法(Compacting Collector)

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

  copying算法(Coping Collector)

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

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

  generation算法(Generational Collector)

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

  adaptive算法(Adaptive Collector)

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

  透視Java垃圾回收

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

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

  java verbosegc classfile

  可以看個例子

  class TestGC

  {

  public static void main(String[] args)

  {

  new TestGC();

  Systemgc();

  SystemrunFinalization();

  }

  }

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

  [Full GC K>K(K) secs]

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

  finalize方法透視垃圾收集器的運行

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

  protected void finalize() throws Throwable

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

  之所以要使用finalize()是由於有時需要采取與Java的普通方法不同的一種方法通過分配內存來做一些具有C風格的事情這主要可以通過固有方法來進行它是從Java裡調用非Java方法的一種方式C和C++是目前唯一獲得固有方法支持的語言但由於它們能調用通過其他語言編寫的子程序所以能夠有效地調用任何東西在非Java代碼內部也許能調用C的malloc()系列函數用它分配存儲空間而且除非調用了 free()否則存儲空間不會得到釋放從而造成內存漏洞的出現當然free()是一個C和C++函數所以我們需要在finalize()內部的一個固有方法中調用它也就是說我們不能過多地使用finalize()它並不是進行普通清除工作的理想場所

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

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

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

  class Chair {

  static boolean gcrun = false;

  static boolean f = false;

  static int created = ;

  static int finalized = ;

  int i;

  Chair() {

  i = ++created;

  if(created == )

  Systemoutprintln(Created );

  }

  protected void finalize() {

  if(!gcrun) {

  gcrun = true;

  Systemoutprintln(Beginning to finalize after + created + Chairs have been created);

  }

  if(i == ) {

  Systemoutprintln(Finalizing Chair # +Setting flag to stop Chair creation);

  f = true;

  }

  finalized++;

  if(finalized >= created)

  Systemoutprintln(All + finalized + finalized);

  }

  }

  public class Garbage {

  public static void main(String[] args) {

  if(argslength == ) {

  Systemerrprintln(Usage: \n + java Garbage before\n or:\n + java Garbage after);

  return;

  }

  while(!Chairf) {

  new Chair();

  new String(To take up space);

  }

  Systemoutprintln(After all Chairs have been created:\n + total created = + Chaircreated +

   total finalized = + Chairfinalized);

  if(args[]equals(before)) {

  Systemoutprintln(gc():);

  Systemgc();

  Systemoutprintln(runFinalization():);

  SystemrunFinalization();

  }

  Systemoutprintln(bye!);

  if(args[]equals(after))

  SystemrunFinalizersOnExit(true);

  }

  }

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

  關於垃圾收集的幾點補充

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

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

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

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

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

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

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

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

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

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

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

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

  結束語

  一般來說Java開發人員可以不重視JVM中堆內存的分配和垃圾處理收集但是充分理解Java的這一特性可以讓我們更有效地利用資源同時要注意finalize()方法是Java的缺省機制有時為確保對象資源的明確釋放可以編寫自己的finalize方法

  轉自:


From:http://tw.wingwit.com/Article/program/Java/hx/201311/25939.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.