1. 程式人生 > >Java GC 演算法總結

Java GC 演算法總結

引言 

  Java的堆是一個執行時資料區,類的例項(物件)從中分配空間。Java虛擬機器(JVM)的堆中儲存著正在執行的應用程式所建立的所有物件,這些物件通過new、newarray、anewarray和multianewarray等指令建立,但是它們不需要程式程式碼來顯式地釋放。一般來說,堆的是由垃圾回收 來負責的,儘管JVM規範並不要求特殊的垃圾回收技術,甚至根本就不需要垃圾回收,但是由於記憶體的有限性,JVM在實現的時候都有一個由垃圾回收所管理的堆。垃圾回收是一種動態儲存管理技術,它自動地釋放不再被程式引用的物件,按照特定的垃圾收集演算法來實現資源自動回收的功能。 

  垃圾收集的意義

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


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

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


垃圾收集的演算法分析

  Java語言規範沒有明確地說明JVM使用哪種垃圾回收演算法,但是任何一種垃圾收集演算法一般要做2件基本的事情:(1)發現無用資訊物件;(2)回收被無用物件佔用的記憶體空間,使該空間可被程式再次使用。 

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


 1、 引用計數法(Reference Counting Collector) 

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

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

  2、tracing演算法(Tracing Collector) 

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

3、compacting演算法(Compacting Collector) 

  為了解決堆碎片問題,基於tracing的垃圾回收吸收了Compacting演算法的思想,在清除的過程中,演算法將所有的物件移到堆的一端,堆的另一端就變成了一個相鄰的空閒記憶體區,收集器會對它移動的所有物件的所有引用進行更新,使得這些引用在新的位置能識別原來 的物件。在基於Compacting演算法的收集器的實現中,一般增加控制代碼和控制代碼表。
4、copying演算法(Coping Collector) 

   該演算法的提出是為了克服控制代碼的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個物件 面和多個空閒面, 程式從物件面為物件分配空間,當物件滿了,基於coping演算法的垃圾 收集就從根集中掃描活動物件,並將每個 活動物件複製到空閒面(使得活動物件所佔的記憶體之間沒有空閒洞),這樣空閒面變成了物件面,原來的物件面變成了空閒面,程式會在新的物件面中分配記憶體。 

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

5、generation演算法(Generational Collector) 

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

 6、adaptive演算法(Adaptive Collector)

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

  1、命令列引數透視垃圾收集器的執行

  2、使用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秒(這個時間在每次執行的時候會有所不同)。

  2、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 = 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物件的建立過程。

關於垃圾收集的幾點補充 

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

  (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,暗示垃圾收集器來收集該物件,還必須注意該引用的物件是否被監聽,如果有,則要去掉監聽器,然後再賦空值。 

  結束語

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

相關推薦

Java GC 演算法總結

引言   Java的堆是一個執行時資料區,類的例項(物件)從中分配空間。Java虛擬機器(JVM)的堆中儲存著正在執行的應用程式所建立的所有物件,這些物件通過new、newarray、anewarray和multianewarray等指令建立,但是它們不需要程式程式碼來顯式地釋放。一般來說,堆的是由垃圾回收

Java 虛擬機器 gc演算法總結

一、垃圾收集基本的演算法 1. 引用計數(Reference Counting) 為每一個物件新增一個計數器,計數器記錄了對該物件的活躍引用的數量。如果計數器為0,則說明這個物件沒有被任何變數所引用,即應該進行垃圾收集。 收集過程如下: 1)減少被收集物件所引用的物件的計數

(5) Java GC演算法及種類

GC演算法主要有以下三種方法(都是以GC Roots可達性為依據,引用計數演算法實現簡單,但由於存在迴圈引用問題,故已不採用,詳見:Java GC(概述)) (1).複製收集演算法 針對Youn

jvm系列(三):java GC演算法 垃圾收集器

GC演算法 垃圾收集器 概述 垃圾收集 Garbage Collection 通常被稱為“GC”,它誕生於1960年 MIT 的 Lisp 語言,經過半個多世紀,目前已經十分成熟了。 jvm 中,程式計數器、虛擬機器棧、本地方法棧都是隨執行緒而生隨執行緒而滅,棧幀隨著方法的進入和退出做入棧和出棧

Java GC 總結

概述 現在 垃圾回收算法 靜態 image 了解 GC垃圾收集器 java 收集 Java GC垃圾收集器 概述 關於JavaGC,有三件事情需要了解 1.那些內存需要回收 2.什麽時候回收 3.這麽回收 這麽判斷對象已死 引用計數法 用一個計數器表示對象的引用,對象被引用

Java 常用排序演算法總結

氣泡排序:  /*冒泡演算法*/ public class BubbleSort { public static void bubble_sort(int[] arr){ int temp; for(int i = 0; i < arr

螞蟻金服 java三面:metaQ訊息中介軟體 + GC演算法 + MySQL資料同步

一面: JVM資料儲存模型,新生代、年老代的構造? java GC演算法,什麼時候會觸發minor gc,什麼時候會觸發full gc? GC 可達性分析中哪些算是GC ROOT? 你熟悉的JVM調優引數,使用過哪些調優工具? Java 有什麼鎖型別?

Java虛擬機器:GC演算法深度解析

   在前面的文章裡介紹了可達性分析演算法,它為我們解決了判定哪些物件可以回收的問題,接下來就該我們的垃圾收集演算法出場了。不同的垃圾收集演算法有各自不同的優缺點,在JVM實現中,往往不是採用單一的一種演算法進行回收,而是採用幾種不同的演算法組合使用,來達到最好的收集效果。接下來

Java GC回收機制總結

一、GC概要   JVM堆相關知識     為什麼先說JVM堆?     JVM的堆是Java物件的活動空間,程式中的類的物件從中分配空間,其儲存著正在執行著的應用程式用到的所有物件。這些物件的建立方式就

排序演算法總結(含動圖演示和Java程式碼實現)

本文將圍繞氣泡排序、桶排序、計數排序、堆排序、插入排序、並歸排序、快速排序和選擇排序,按照描述、時間複雜度(最壞情況)、動態圖展示和程式碼實現來講解。本文預設排序為從小到大。 本文相關程式碼已上傳至github,歡迎關注https://github.com/zhuzhenke/commo

Java 八大排序演算法總結

概述 排序有內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。我們這裡說說八大排序就是內部排序。 插入排序 思想:每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的子序列

JVM GC調優(2)-----GC演算法判定物件可以被回收(部分摘自深入理解Java虛擬機器)

這次我們介紹JVM中的GC演算法 引用計數法 可達性分析法 首先我們提出四個問題 哪裡的記憶體需要回收? 什麼時候回收? 如何回收? Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的“高牆”, 牆外面的人想進去, 牆裡面的人卻想出來。

JVM GC調優(3)-----GC演算法(部分摘自深入理解Java虛擬機器)

介紹幾種GC演算法的思想及其發展過程: 標記-清除 複製 標記-壓縮 分代收集演算法 GC演算法主要是用於堆死亡物件的清理的集中方式,他們各有優缺點,下面我們開始做介紹 標記清除演算法 原理解析 -最基礎的收集演算法是“標記-清除”( Mark-Swe

Java常用演算法原理、實現及總結

一、概念 1、內部排序與外排序 內部排序:待排序記錄存放在計算機隨機儲存器中(說簡單點,就是記憶體)進行的排序過程,接下來講的十大演算法都是內部排序。 外部排序:待排序記錄的數量很大,以致於記憶體不能一次容納全部記錄,所以在排序過程中需要對外存進行訪問的排序過程

Java 八種排序演算法總結

前言 一、簡介  二、演算法複雜度 三、常見演算法 (1)氣泡排序 (2)選擇排序 (3)插入排序 (4)歸併排序 (5)快速排序 (6)希爾排序 (7)基數排序 (8)堆排序 四、總結 五、Demo地址 六、參考文件

JAVA 虛擬機器 GC演算法GC回收器

本章內容 標記-清除演算法 複製演算法  標記-整理演算法 分代收集演算法 七種垃圾收集器   標記-清除演算法 標記;遍歷所有的GC Roots,然後將所有GC Roots可達的物件標記為存活的物件 清除:清除的過程將遍歷堆中所有的物件,將沒有標記的物件全部清除

java finalize方法總結 GC執行finalize的過程

(1) 首先,大致描述一下finalize流程:當物件變成(GC Roots)不可達時,GC會判斷該物件是否覆蓋了finalize方法,若未覆蓋,則直接將其回收。否則,若物件未執行過finalize方法,將其放入F-Queue佇列,由一低優先順序執行緒執行該佇列中物件的finalize方法。執行finaliz

十大經典排序演算法總結——Java實現

轉自:https://blog.csdn.net/wangqyoho/article/details/52584640#t5 引 這段時間博主逐步替換為Java的實現 //博主留 2017.9.15 //2017.10.10完成氣泡排序的修改

常用排序演算法總結Java實現)

排序演算法比較: 1. 氣泡排序 /** * 氣泡排序 * 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。 * 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。 * 針對所有的元素重複以上的步驟,除了最後一個

GC垃圾回收演算法總結

閒著沒事總結下java虛擬機器的垃圾回收演算法。 哪些物件會被回收 兩種方式: 第一種:引用計數法:只要一個物件被引用,計數就+1,釋放引用,就-1,但是無法解決兩個物件互相引用的問題 第二種:物件可達狀態分析 主要包括: 虛擬機器棧中引用的物件。