深刻學習Java虛擬機之——垃圾收集算法與垃圾收集器

今天咱們將一塊兒學習Java虛擬機使用垃圾收集算法和常見的垃圾收集器。Java虛擬機內存區域的程序計數器、虛擬機棧和本地方法棧3個區域是隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出出棧和入棧。每個棧幀中分配多少內存基本上是在類結構肯定下來的時候就已知的,所以這個幾個區域的內存分配和回收都具有肯定性,在這幾個區域就不須要過多考慮回收問題,由於方法結束或者線程結束時,內存天然就跟着回收了。而Java堆和方法區就不同,一個接口中的多個類實現須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間是才能知道會建立哪些對象,這部份內存和回收是動態的,垃圾收集器所關注的是這部份內存。java

1、判斷對象是否存活算法

在垃圾收集器對對象進行回收前,首先須要判斷哪些對象是存活的。bash

一、引用計數算法多線程

給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任什麼時候候計數器爲0的對象就是不可能在被使用的。可是這樣算法的存在的問題是很難解決對象之間相互循環引用的問題。當前主流的Java虛擬機都沒有采用這樣的算法。咱們看以下列子:testGC()方法執行後,objA和objB會不會被GC呢?併發

package gc;

public class ReferenceCountingGC
{
	public Object instance =null;
	 
	private static final int _1MB=1024*1024;
	
	private byte[] bigSize=new byte[2*_1MB];
	
	public static void testGC()
	{
		ReferenceCountingGC objA=new ReferenceCountingGC();
		ReferenceCountingGC objB=new ReferenceCountingGC();
		
		objA.instance=objB;
		objB.instance=objA;
		
		objA=null;
		objB=null;
		
		System.gc();
	}
	
	public static void main(String[] args)
	{
		testGC();
	}
}

GC日誌輸出結果:ide

[GC (System.gc()) [PSYoungGen: <strong>5735K->584K</strong>(18944K)] 5735K->592K(62976K), 0.0008309 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen:<strong> 584K->0K</strong>(18944K)] [ParOldGen: 8K->514K(44032K)] 592K->514K(62976K), [Metaspace: 2502K->2502K(1056768K)], 0.0058089 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 18944K, used 491K [0x00000000eb400000, 0x00000000ec900000, 0x0000000100000000)
  eden space 16384K, 3% used [0x00000000eb400000,0x00000000eb47aff0,0x00000000ec400000)
  from space 2560K, 0% used [0x00000000ec400000,0x00000000ec400000,0x00000000ec680000)
  to   space 2560K, 0% used [0x00000000ec680000,0x00000000ec680000,0x00000000ec900000)
 ParOldGen       total 44032K, used 514K [0x00000000c1c00000, 0x00000000c4700000, 0x00000000eb400000)
  object space 44032K, 1% used [0x00000000c1c00000,0x00000000c1c808e8,0x00000000c4700000)
 Metaspace       used 2511K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 273K, capacity 386K, committed 512K, reserved 1048576K

對象objA和對象objB都有字段instance,進行objA.instance=objB及objB.instance=objA賦值操做,實際上着兩個對象再無任何引用,他們互相引用對方,致使引用計數都不爲0,因而引用計數算法沒法通知GC收集器回收他們。可是結果是被收回了(5735K->584K),因此咱們的虛擬機採用的不是引用計數算法。佈局

二、 可達性分析算法性能

在主流的商用程序語言的主流實現中,都是經過可達分析算法來判斷對象是否存活的。這個算法的基本思想就是經過一系列成爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑成爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈是,則證實對象是不可用的,將會被斷定爲可回收的對象,可是不會被當即回收,須要被標記兩次以後纔會被回收。學習

2.1 Java語言中,可做爲GC Roots的對象有:網站

1) 虛擬機棧(棧幀中本地變量表)中引用的對象。

2)方法區中類靜態屬性引用的對象。

3)方法區中常量引用的對象。

4) 本地方法棧中JNI(通常說的Native方法)引用的對象。

2、引用

不管是引用計數法判斷對象的引用數量,仍是可達性分析算法判斷對象的引用鏈是否可達,判斷對象是否存活都與「引用」有關。Java中的引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這種引用強度一次逐漸減弱。

1) 強引用就是指在程序代碼之中廣泛存在的,相似"Object obj=new Object()"這類的引用,只要強引用存在,垃圾收集器永遠不會回收掉被引用的對象。

2) 軟引用是用來描述一些還有用但並不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存異常。在JDK1.2以後,提供了SoftReference類來實現軟引用。

3) 弱引用也是用來描述非必須對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象,在JDK1.2以後,提供了WeakReference類來實現弱引用。

4) 虛引用是最弱的一種引用關係。一個對象是否全部虛引用存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯惟一目的就是能在這個對象唄收集器回收時獲得一個系統通知。在JDK1.2後,提供了PhantomReference類來實現虛引用。

3、對象的自我拯救

即便在可達分析算法中不可達的對象,也並不是當即就被回收,須要通過兩次標記。若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者finalize()方法被虛擬機已經調用過,虛擬機將這兩種都視爲沒有必需要執行。

若是這個對象被斷定爲由必要執行finalize()方法,那麼這個對象將會放置在一個F-Queue的隊列中,並在稍後由一個虛擬機自動創建的、低優先級的Finalizer線程去執行。若是對象在finalize()方法中從新與引用鏈上的任何對象創建關聯便可,好比把本身(this關鍵字)賦值給某個類的變量或者對象的成員變量,那在第二次標記是它將被移除即將回收的集合;若是對象這個時候尚未逃脫,那基本上他就被回收了。來看以下代碼:

package gc;

/**
 * 此代碼演示了兩點
 * 一、對象能夠在被GC時自我拯救
 * 二、這種自救機會只有一次
 * @author Administrator
 *
 */
public class FinalizeEscapeGC
{
	public static FinalizeEscapeGC SAVE_HOOK=null;
	
	public void isAlive()
	{
		System.out.println("yes,i am still alive");
	}
	
	@Override
	protected void finalize() throws Throwable
	{
		super.finalize();
		System.out.println("finalize method executed");
		
		FinalizeEscapeGC.SAVE_HOOK=this;
	}
	
	public static void main(String[] args) throws Throwable
	{
		SAVE_HOOK=new FinalizeEscapeGC();
		
		//對象第一次拯救本身
		SAVE_HOOK=null;
		System.gc();
		
		//由於finalize方法優先級很低,暫停0.5秒等待它
		Thread.sleep(500);
		if(SAVE_HOOK!=null)
		{
			SAVE_HOOK.isAlive();
		}else
		{
			System.out.println("no, i am dead");
		}
		
		//再次拯救,失敗
		SAVE_HOOK=null;
		System.gc();
				
		Thread.sleep(5000);
		if(SAVE_HOOK!=null)
		{
			SAVE_HOOK.isAlive();
		}else
		{
			System.out.println("no, i am dead");
		}
		
	}
}

輸出結果:

finalize method executed
yes,i am still alive
no, i am dead

第一次拯救成功,第二次卻失敗。任何一個對象的finalize()方法都只會被對象自動調用一次,若是對象面臨下一次回收,它的finalize()方法會被再次執行。須要注意的時,最好不要使用該方法來拯救對象。

4、回收方法區

在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而在方法區的垃圾收集效率遠低於此。方法區的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是類似。以常量池中字面量的回收爲例,例如一個字符串「abc"已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作」abc「的,換句話說,就是沒有任何String對象引用常量池中的」abc"常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc"常量就會被系統清理出常量池。常量池中其餘類、方法、字段的符號引用也與此相似。

斷定一個類爲無用類的3個條件:

1) 該類全部實力都已經被回收,也就是Java堆中不存在該類的任何實例。

2)加載該類的ClassLoader已經被回收。

3) 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

5、垃圾收集算法

一、標記-清除算法

算法分爲標記清除兩個階段:首先標記出所須要回收的對象,在標記完成後統一回收全部標記的對象,它的標記過程就是上面提到的。這種算法主要有兩個不足:一是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配打對象時,沒法找到足夠的連續內存而不得不提早出發另外一次垃圾收集動做。

二、複製算法

它將可能內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可。這種算法效率高,代價是將內存壓縮爲原來的一半。如今的商業虛擬機都採用這種手機算法來回收新生代。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,固然這樣沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用的時候,須要依賴其餘內存(老年代)進行分配擔保。

三、標記-整理算法

複製收集算法在對象存活率較高是就要進行較多的複製操做,效率將會變低。根據老年代的特色,提出了標記-整理算法,標記過程任然與「標記-清除算法」同樣,可是後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

四、 分代收集算法

當代商業虛擬機的垃圾收集算法都採用「分代收集算法」,根據對象存活週期的不一樣將內存劃分爲幾塊。通常是Java堆中分爲新生代和老年代,這樣就能夠根據各個年代的特色採用歲適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象的存活率高,沒有額外空間對它進行擔保,就必須採用「標記-清理算法」或者「標記-整理算法」來實現回收。

6、垃圾收集器(針對HotSpot虛擬機而言)

一、Serial收集器

這個收集器時一個單線程的收集器,進行垃圾收集時,必須暫停其餘全部工做線程,直到垃圾收集結束。目前用於Client模式下的默認新生代收集器。其優勢是,簡單而高效(與其餘收集器的單線程相比),對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,垃圾收集效率十分高。

二、ParNew收集器

ParNew收集器時Serial收集器的多線程版本,除了使用多線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數、收集算法、Stop The World、對象分配規則等與Serial收集器同樣。目前是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中一個與性能無關的但很重要的緣由是,除了Serial收集器外,目前只有他能與CMS(Concurrent Mark Sweep)收集器配個工做。

三、Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。該收集器的目標是達到一個可控 的吞吐量。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(用戶代碼運行時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉了1分鐘,那吞吐量就是99%。

高吞吐量能夠高效率得利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

四、 Serial Old收集器

Serial Old收集器時Serial收集器的老年代版本,一樣也是一個單線程收集器,使用「標記-整理算法」。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下,它還有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途就是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

五、 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。在注重吞吐量以及CPU資源銘感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。

六、 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以得到最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間短暫,以給用戶帶來良好的體驗。CMS是一種基於「標記-清除」算法實現的 ,運做過程分爲四個步驟:

1)初始標記

2)併發標記

3)從新標記

4)併發清除

其中,初始標記、從新標記這兩個步驟仍然須要「stop the world"。初始標記僅僅實在是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而從新標記階段則是爲了修改併發標記期間由於用戶程序持續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍微長一些,但遠比並發標記時間短。

因爲整個過程耗時最長的併發標記和併發清除過程收集線程均可以與用戶線程一塊兒工做,因此,整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

缺點以下:

1)CMS收集器對CPU資源很是敏感,在併發的時候佔用CPU資源會致使應用程序變慢,總吞吐量會下降,在CPU數量少的狀況下會很明顯。

2)CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure"失敗而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然還會有新的垃圾產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉他們,這一部分叫作」浮動垃圾「。

3) 因爲CMS是基於「標記-清除」算法實現的收集器,這必然會產生不少空間碎片,將會給大對象分配帶來很大的麻煩,每每老年代還有很大的空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得再也不次出發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認開啓),用於CMS收集器頂不住要進行Full GC是開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間不得不變長。

七、G1收集器

G1(Garbage-First)收集器時當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器,與其餘GC收集器相比,G1具有以下特色:

1) 並行與併發:G1能容許利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。

2)分代收集:與其餘收集器同樣,分代概念在G1中依然得以保留。雖然G1能夠不須要其餘收集器配個就能獨立管理整個GC堆,但它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以得到更好的收集效果。

3)空間整合:與CMS的標記-清除算法不一樣,G1從總體來看是採用基於標記-整理算法實現的收集器,從局部上來看是基於複製算法實現的。這兩種算法都不會在G1運行期間產生內存空間碎片,收集後可以提供規整的可用內存。

4)可預測的停頓:這是G1相對於CMS的另外一優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

在G1以前的其餘收集器進行收集的範圍是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,他將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不在是物理隔離的了,他們都是一部分Region的集合。

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值大的Region。

關於垃圾收集器就到這裏,細節的地方就不在這裏多說了。

參考:

《深刻java虛擬機》

相關文章
相關標籤/搜索