虛擬機學習之二:垃圾收集器和內存分配策略

1.對象是否可回收

1.1引用計數算法

引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候候計數器值爲0的對象就是不可能再被使用的對象。java

客觀來講,引用計數算法的實現簡單,斷定效率高,在大部分狀況下都是不錯的算法,可是在主流的java虛擬機裏面都沒有選用該算法進行內存管理,主要緣由是它很難解決對象之間相互循環引用的狀況。以下面代碼例子:算法

配置:輸出垃圾回收日誌數組

-XX:+PrintGC

 

代碼: 安全

public class ReferenceCountingGC {

	private ReferenceCountingGC instance = null;

	private static final int _1M = 1024 * 1024;

	private byte[] bsize = new byte[2 * _1M];

	public static void testGC() {
		ReferenceCountingGC rc1 = new ReferenceCountingGC();
		ReferenceCountingGC rc2 = new ReferenceCountingGC();
		
		//兩個對象互相引用
		rc1.instance = rc2;
		rc2.instance = rc1;
		
		rc1 = null;
		rc2 = null;
		//提醒虛擬機執行垃圾回收
		System.gc();
	}
	
	public static void main(String[] args) {
		testGC();
	}

}

運行結果:服務器

[GC (System.gc())  6092K->736K(125952K), 0.0009621 secs]
[Full GC (System.gc())  736K->612K(125952K), 0.0068694 secs]數據結構

 從運行結果中能夠清楚看到,GC日誌中包含6092K->736K,意味着虛擬機並無由於這兩個對象相互引用就不回收它們,這也從側面說明虛擬機並非經過引用計數算法來判斷對象是否或者。(配置-XX:+PrintGC或者-verbose:gc輸出基本回收信息,配置-XX:+PrintGCDetails能夠輸出詳細的GC信息)。多線程

1.2可達性分析算法

在虛擬機的主流實現中,都是經過可達性分析算法來斷定對象是否存活的。這個算法的基本思路就是:經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(也就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。併發

在java語言中能夠做爲「GC Roots」的對象包括如下幾種:jvm

  • 虛擬機棧中引用的對象,也就是棧幀中本地變量表中的對象。
  • 方法區中靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(通常說的是Native方法)引用的對象。

以下面圖中所示:雖然obj五、obj六、obj7之間相互引用可是它們到GC Roots沒有可達的調用鏈,因此他們將會被斷定爲可回收的對象。ide

1.3對象引用類別

在JDK1.2以前定義引用:若是reference類型的數據中存儲的數值表明另外一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義雖然比較純粹可是太過狹隘,咱們實際中更但願能表明一種狀況:當內存空間足夠時,則保留在內存之中,當內存空間在進行垃圾回收以後依然比較緊張,則能夠拋棄這些對象。因此在JDK1.2以後java就對引用概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)。這四種引用強度依次減弱。

  • 強引用:這種引用在代碼中廣泛存在,相似「Object obj = new Object()」這類的引用只要引用還在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用:這種引用用來描述一些「還有用但並不是必須」的對象,對於軟引用關聯的對象,在系統將要發生內存溢出以前,會將這些對象列入回收對象之中進行二次回收,若是回收以後依然沒有足夠的內存,纔會拋出內存溢出異常。
  • 弱引用:是用來描述非必須對象的,但它的強度比軟引用更弱一些,被若引用關聯的對象只能生存到下一次垃圾回收以前,當垃圾收集器工做時,不管當前內存是否足夠都會回收掉被弱引用關聯的對象。
  • 虛引用:也被成爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用存在徹底不會對其生存時間產生影響,也不能經過虛引用取得一個對象實例。爲對象設置虛引用的惟一目的就是可以在對象被回收時收到一個通知。

1.4finalize方法

即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候他們暫時處於「緩刑」階段,要真正宣佈一個對象死亡,至少要經理兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的調用鏈,那麼這個對象將會被進行第一次標記而且進行一次篩選,篩選的條件就是是否有必要執行finalize方法,若是沒有覆蓋該方法或者已經被虛擬機調用過,就會被認爲「沒有必要執行」。若是被斷定爲有必要執行finalize方法,就會將對象放置在一個叫作「F-Queue」的隊列中,並由一個虛擬機自建的優先級低的線程去執行它(虛擬機只是觸發調用,並不保證執行成功或完成)。若是在執行finalize方法的過程當中對象從新與引用鏈上的任何一個對象創建關聯則在稍後的第二次標記中該對象就會被移除「即將回收隊列」,若是這時候依然沒有和引用鏈上的對象創建關聯,則該對象就會被回收。虛擬機調用對象finalize方法只有一次,不會進行第二次調用。以下代碼實例:

代碼:

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 Exception {
		SAVE_HOOK = new FinalizeEscapeGC();
		//對象第一次拯救本身
		SAVE_HOOK = null;
		System.gc();
		//由於虛擬機調用finalize方法優先級比較低,暫停1s等待執行。
		Thread.sleep(1000);
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("I am dead!");
		}
		
		//第二次時不會再調用finalize方法
		SAVE_HOOK = null;
		System.gc();
		Thread.sleep(1000);
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("I am dead!");
		}
		
		
	}
}

執行結果:

finalize method executed!
yes,I am still alive!
I am dead!

能夠看到回收的第一次執行了finalize方法而後對象沒有被回收,第二次時沒有調用finalize方法,對象被回收掉了。

這種方法雖然能在對象被回收時自救一次,但在編寫代碼時不建議使用此種操做。

1.5回收方法區(JDK8中是回收元空間)

按照JDK7介紹,永久代中的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。

廢棄常量:廢棄常量的回收和java堆中對象的回收很是相似。以常量池爲例,若是一個字符串「abc」已經進入常量池中,可是當前系統中沒有任何一個String字符串對象叫作「abc」,也就是沒有任何一個對象引用這個「abc」常量,也沒有任何一個地方引用這個字面量。這個時候發生內存回收,有必要的話常量池中的「abc」常量會被系統清理出常量池。

無用的類:類的回收斷定比較嚴格要知足一下三個條件才能夠會被回收。

  • 該類全部的實例都已經被回收。
  • 加載該類的ClassLoader也已經被回收。
  • 該類對應的class對象也沒有在任何地方被引用,也就是不能經過反射訪問該類。

2.垃圾收集算法

2.1標記-清除算法

標記-清除算法:最基礎的收集算法,主要分爲「標記」和「清除」兩個階段完成,首先標記出全部須要回收的對象,在標記完成以後進行統一回收。之因此說它時最基礎的收集算法是由於後續的收集算法都是基於這種思路進行對其不足進行改進而獲得的。

這種算法有兩種不足:第一個就是這兩個階段的效率都不高;第二個是在標記清除以後會產生大量的不連續的內存碎片,空間碎片太多可能會致使在後面程序運行過程當中若是分配較大對象時,沒法找到足夠的連續內存空間,而不得不提早進行下一次垃圾回收。

2.2複製算法

複製算法:將可用內存分爲大小相等的兩部分,每次只使用其中的一塊,當這一塊內存用完,就進行垃圾回收將還存活的對象複製到另外一塊上面,而後清除掉剛纔使用的內存空間。這樣作的好處就是每次對整個板塊內存進行回收,不用考慮內存碎片等複雜問題。可是這種算法的代價就是講內存可用空間直接縮小了一半。

在商業虛擬機中都使用這種算法來回收新生代。IBM研究代表大部分狀況新生代中有98%的對象會被第一次收集時被回收掉,因此在實現中把內存分爲較大的一塊Eden空間和兩個較小的Survivor空間,比例是8:1:1.每次使用時將新建立的對象分配到Eden區其中一個Surivivor區保存上次回收存活下來的對象,當進行垃圾收集時將Eden和使用中的Survivor中的存活對象複製到另外一個Survivor中,而後清空Eden和使用過的Survivor空間,依次循環使用。固然並非每次存活的對象都不足10%,當存活對象大於10%時Surivivor中的空間就不夠使用,就須要依賴其餘內存進行分配擔保(老年代)。也就是當另外一塊Surivivor內存不夠時就會將存活的對象分配到老年代中。

2.3標記-整理算法

複製算法在對象存活率較高時就要進行較多的複製操做,從而下降效率。還要預留擔保空間,以應對存活對象較多時新生代內存不夠分配的狀況。因此在老年代提出了「標記-整理」算法,標記一樣跟前面的「標記-清除」算法中標記操做同樣,可是標記以後不會將對象清除掉,而是將對象移動到整塊內存空間的一端,而後直接清理掉邊界之外的內存。

2.4分代收集算法

當前商業虛擬機都採用「分代收集算法」,這種算法只是將整塊內存按照對象存活週期分爲幾個塊,通常把java堆分爲新生代、老年代。這樣就能夠根據各個年代特色使用不一樣算法進行收集。例如在新生代每次回收時都有少許對象能夠存活,就是用複製算法,將少許存活對象複製到Survivor區。而老年代對象存活率比較高只有少許對象會被清除掉,就選用「標記-清除」或者「標記-整理」算法。

3.HotSpot算法實現

3.1枚舉根節點

在可達性分析算法中能夠做爲GC Roots節點的主要在全局性引用(例如常量、靜態屬性)或者執行上下文(棧中本地變量表)中。在查找調用鏈的時候並不會這個檢查這裏面的引用,由於這樣會消耗不少時間。

HotSopt實現中,使用一組稱爲OopMap的數據結構,在類加載完成的時候就已經計算出來對象「哪些」偏移量上面存儲「哪些」數據類型。例如:在JIT編譯過程當中會在特定位置記錄棧和寄存器中哪些位置是引用。這樣在GC掃描的時候能夠直接引用。

另外在執行GC 的時候全部java線程都必須停頓下來(Stop The World),由於在執行可達性分析算法的時候對象的引用關係不能發生變化。

3.2安全點

上面提到記錄引用的特定位置稱爲「安全點」,線程在GC的時候須要暫停執行,但並非在任何地方均可以停下來的,須要線程跑到「安全點」上時若是這時候有GC標識就暫停執行,這樣能夠保證在GC時引用不會發生變化。

3.3安全區域

安全區域:指在一段代碼片斷之中,引用關係不會發生變化。這個區域中的任何位置開始GC 都是安全的。

4.垃圾收集器

4.1Serial收集器

新生代收集器,複製算法。

Serial收集器是最基本、發展歷史最悠久的收集器。這個收集器是一個單線程的收集器,它有一條專門的線程負責垃圾收集工做,更重要的是它在垃圾回收的時候要中止全部其餘線程。主要用在Client模式下(用戶的桌面場景中)。

4.2ParNew收集器

新生代收集器,複製算法。

ParNew收集器是Serial收集器的多線程版本。除了使用多線程進行垃圾收集以外其餘的全部包括控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial徹底同樣。

4.3Paralle Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,也是使用複製算法的收集器,有事並行的多線程收集器。

Paralle Scavenge收集器的特色就是關注吞吐量:運行用戶代碼時間 / CPU總運行時間(用戶代碼時間+垃圾收集時間)。

Paralle Scavenge收集器提供了能夠配置精準控制吞吐量的參數。因此又稱爲「吞吐量優先」收集器。

4.4Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,一樣是一個單線程收集器,使用「標記-整理」算法。

主要給client模式下的虛擬機使用。

4.5Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。和Paralle Scavenge收集器搭配實現名副其實的「吞吐量優先」收集器。

4.6CMS收集器

CMS收集器(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間爲目標的收集器。CMS收集器主要分四個步驟:

  • 初始標記
  • 併發標記
  • 從新標記
  • 併發清除

初始標記、從新標記:這兩個步驟雖然很快可是仍是須要「Stop The World」。

併發標記:進行GC Roots tracing,在這個階段jvm收集線程會和用戶線程並行執行。(時間較長,下降用戶系統信息)。

缺點:

  • 佔用用戶CPU資源,4核以上服務器至少佔用1/4CPU資源。
  • 產生「浮動垃圾」因爲CMS收集器和用戶線程併發執行,在收集過程當中用戶線程可能產生新的垃圾對象。
  • 標記清除算法產生碎片內存空間,屢次執行標記清除回收以後要進行一次內存壓縮。

4.7G1收集器

G1收集器是當今收集技術最前沿成果之一。

特色:

  • 並行和併發,縮短「Stop The World」時間,讓用戶線程和收集線程併發執行。
  • 分代收集
  • 空間整合,不會產生內存碎片。
  • 可預測停頓時間,可讓使用者明確指定在一個長度爲M毫米的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。

G1以前的收集器收集範圍都是整個新生代或者老年代。而G1收集器將整個java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留着新生代和老年代,但新生代和老年代再也不是物理隔離的了,他們都是有一部分Region的集合組成。G1維護一個優先列表記錄每一個Region回收的價值大小,每次根據容許收集時間,首先回收價值最大的Region。

4.8理解GC日誌

JVM中能夠配置使用不一樣的收集器,不一樣收集器輸出的日誌格式雖然相同,但每種收集器都有本身的標識。

例如:

一、配置:"-XX:+UseSerialGC" 使用Serial+Serial Old的收集器組合進行內存回收。

日誌格式:[GC (Allocation Failure) [DefNew: 7292K->612K(9216K), 0.0055084 secs] 7292K->6756K(19456K), 0.0055700 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

二、配置:"-XX:+UseParallelOldGC" 使用Paralle Scavenge + Parallel Old的收集器組合進行內存回收。

日誌格式:[GC (Allocation Failure) --[PSYoungGen: 7292K->7292K(9216K)] 11388K->15492K(19456K), 0.0030434 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 7292K->2658K(9216K)] [ParOldGen: 8200K->8193K(10240K)] 15492K->10851K(19456K), [Metaspace: 2664K->2664K(1056768K)], 0.0078503 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

JDK8 HotSopt虛擬機默認使用的是並行收集器,日誌以下格式進行講解。

[GC (System.gc()) [PSYoungGen: 1331K->32K(38400K)] 1943K->644K(125952K), 0.0004471 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 612K->611K(87552K)] 644K->611K(125952K), [Metaspace: 2662K->2662K(1056768K)], 0.0076396 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

[GC 和[Full GC 表明收集停頓來下,Full表示有停頓及「Stop The World」。

[PSYongGen 和 [ParOldGen、[Metaspace表示GC發生的區域,[PSYongGen新生代;[ParOldGen老年代;[Metaspace元空間。

區域後面「[ ]」以內的32K->0K(38400K) 表示:該區域回收以前佔用容量->回收以後佔用容量(該區域總容量)。

"[ ]"以外的644K->611K(125952K)表示:java堆GC以前的佔用容量->GC以後佔用容量(java堆總用量)。

5內存分配與回收策略

5.1對象優先在Eden分配

經過例子講解:首先建立4個數組對象allocation一、allocation二、allocation三、allocation4,佔用空間分別爲2M、2M、2M、4M,而後指定虛擬機堆內存20M,新生代內存10M,新生代中Eden區域Survivor區域佔比爲8:1。

public class EdenTest {

	private static final int _1MB = 1024 * 1024;
    /**
	 * -verbose:gc -Xms20M(堆初始大小) -Xmx20M(堆最大值) -Xmn10M(堆中年輕代大小) -XX:+PrintGCDetails -XX:SurvivorRatio=8(表示Eden與一個Survivor比例爲8:1)
	 */
	private static void testAllocation() {
		byte[] allocation1, allocation2, allocation3, allocation4;
		allocation1 = new byte[2 * _1MB];
		allocation2 = new byte[2 * _1MB];
		allocation3 = new byte[2 * _1MB];
		allocation4 = new byte[4 * _1MB];
	}

	public static void main(String[] args) {
		testAllocation();
	}

}

使用Serial+Serial Old收集器組合進行內存回收(UseSerialGC配置指定收集器)

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC

運行日誌:

[GC (Allocation Failure) [DefNew: 7292K->613K(9216K), 0.0050167 secs] 7292K->6757K(19456K), 0.0050693 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4791K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  59% used [0x00000000ff500000, 0x00000000ff599460, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 2668K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

日誌解讀

GC (Allocation Failure):表示向young generation(eden)給新對象申請空間,可是young generation(eden)剩餘的合適空間不夠所需的大小致使的minor gc。

DefNew:表示新生代使用Serial串行GC垃圾收集器,defNew提供新生代空間信息。

7292K->613K(9216K):新生代佔用內存7292K -> 收集器回收以後佔用內存613K(新生代可用內存9216K)。

7292K->6757K(19456K):java堆被佔用內存7292K -> 收集器回收以後佔用內存6757K (堆內存總空間19456K)。

Heap則表示此時堆內存中每一個區域分配內存大小以及被使用的空間比例。

下面咱們經過日誌分析allocation一、allocation二、allocation三、allocation4這四個對象分配的位置。

首先日誌的第一行進行新生代收集出現日誌:7292K->613K(9216K),說明allocation一、allocation二、allocation3這三個對象共計6M大小在Eden區,收集事後使用空間爲613K,對象被移走,正常狀況下Eden區對象首次會被移到其中一個Survivor區,可是Survivor區空間只用1M不足存放6M對象大小,因此這些對象直接被移送到了老年代中。堆內存收集先後並無發生大的變化7292K->6757K(19456K),也表示這些對象還在堆內存中,只是重新生代的Eden區直接被移送到了老年代中。

從最後堆內存各個區域內存佔用的狀況也能夠分析得出咱們的推論

區域 大小 使用比例 說明
新生代:eden 8192K 51% 被佔用4M空間,被allocation4佔用
新生代:from 1024K 59% 空間不足1M,並未存放測試對象
新生代:to   1024K 0% Survivor只有一個被使用
老年代區域 10240K 60%  老年代佔用6M,爲3個2M的對象

5.2大對象直接進入老年代

大對象就是在虛擬機中須要大量連續空間存放的對象。好比字符串對象或數組等。

虛擬提供一個-XX:PretenureSizeThreshold參數,能夠設置令大於該參數值的對象直接進入老年代。這個參數只對Serial和ParNew收集器有效。

public class PretenureSizeThresholdTest {

	private static final int _1MB = 1024 * 1024;

	/**
	 * -verbose:gc -Xms20M(堆初始大小) -Xmx20M(堆最大值) -Xmn10M(堆中年輕代大小)
	 * -XX:+PrintGCDetails -XX:SurvivorRatio=8(表示Eden與一個Survivor比例爲8:1)
	 * -XXPretenureSizeThreshold=3145728(超過該值的對象直接進入老年代)
	 */
	public static void main(String[] args) {

		byte[] allocation;
		allocation = new byte[4 * _1MB];
	}
}

JVM執行參數配置:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728

執行日誌:

Heap
 def new generation   total 9216K, used 1312K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed480b0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

其中the space 10240K,  40% used,老年代使用40%,說明對象直接進入了老年代區域。

5.3長期存活的對象將進入老年代

       虛擬機採用分代收集的思想來管理內存,虛擬機爲每一個對象定義了一個年齡(Age)計數器,若是對象在Eden出生,並經歷一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。此後對象在Survivor中每熬過一次Minor GC對象年齡就增長1歲,當年齡增長到必定程度時(默認15歲),該對象就會被移動到老年代中。對象晉升到老年代的這個閾值能夠經過參數設定 -XX:MaxTenuringThreshold=2,表示對象經理過兩次Minor GC 就能夠被移動到老年代。

5.4動態對象年齡斷定

對象移動到老年代的另外一個規則:當Survivor空間中相同年齡的對象所佔用空間大小的總和大於Survivor空間的一半時,年齡大於或等於該年齡的對象就能夠被直接移動到老年代中。

相關文章
相關標籤/搜索