jvm筆記2--垃圾收集器與內存分配策略

分類:IT技術 時間:2016-10-17

垃圾收集器與內存分配策略

Java運行時,內存的各個部分中,程序計數器,虛擬機棧,本地方法棧3個區域隨線程而生,隨線程而滅:棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧的操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的。因此這幾個區域不需要過多考慮回收的問題,因為線程結束時,內存自然就跟著回收了。 Java堆和方法區不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,只有在程序運行期間才知道會創建哪些對象,這部分內存的分配和回收都是動態的。

1對象死亡

1.引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。 -XX:HeapDumpOnOutOfMemoryError:虛擬機在出現OOM異常後自動生成dump文件。
Java虛擬機裏面沒有選用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題,如:
public class ReferenceCountingGC {

	public Object instance = null;
	// 占點內存,方便觀察內存回收
	private byte[] test = new byte[2 * 1024 * 1024];

	/**
	 * VMArgs: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
	 * -XX:+PrintGCDetails
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		// 對象內相互引用
		objA.instance = objB;
		objB.instance = objA;

		objA = null;
		objB = null;
		system.gc();
	}
}
[Full GC (System) [Tenured: 0K->152K(13696K), 0.0097331 secs] <strong>4340K->152K</strong>(19840K), [Perm : 368K->368K(12288K)], 0.0097823 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 6144K, used 110K [0x33090000, 0x33730000, 0x33730000)
  eden space 5504K,   2% used [0x33090000, 0x330ab8b8, 0x335f0000)
  from space 640K,   0% used [0x335f0000, 0x335f0000, 0x33690000)
  to   space 640K,   0% used [0x33690000, 0x33690000, 0x33730000)
 tenured generation   total 13696K, used 152K [0x33730000, 0x34490000, 0x34490000)
   the space 13696K,   1% used [0x33730000, 0x33756270, 0x33756400, 0x34490000)
 compacting perm gen  total 12288K, used 368K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec3c0, 0x344ec400, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)
由結果可以看出,虛擬機並沒有因為這兩個對象互相引用就不回收它們,也就說明虛擬機並不是通過引用計數算法來判斷對象是否存活的。

2.可達性分析算法

Java語言的主流實現中,都是通過可達性分析(Reachability Analysis)來判定對象是否存活的。基本思路就是通過一系列的稱為"GC Roots"的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明這個對象不可用即可回收的對象。
Java中可以作為GC Roots的對象
  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Native方法)引用的對象

3.引用

定義:如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表著一個引用。 Java將引用分為4種:強度一次逐漸減弱
    • 強引用(Strong Reference):
類似"Object  obj = new Object()"這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用(Soft Reference):
描述一些還有用但並非必需的對象。對於軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果回收後還沒有足夠的內存,才會拋出內存溢出異常。
  • 弱引用(Weak Reference):
描述非必需對象,強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾回收之前。當垃圾回收時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用(Phantom Reference):
也稱為幽靈引用或幻影引用,是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

4.對象死亡

在可達性分析算法中不可達的對象,只是處於"緩刑"階段。真正確定一個對象死亡,至少要經歷兩次標記過程:對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,將會被第一次標記。然後進行一次篩選,條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為"沒有必要執行"。


如果這個對象被判定為有必要執行finalize()方法,這個對象就會放置在一個叫做F-Queue的隊列中,並在稍後由一個由虛擬機自動建立的,低優先級的Finalizer線程去執行(由虛擬機觸發),但並不一定會等待它運行結束。原因就是:如果對象在finalize()方法中執行緩慢,或者發生了死循環,將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。


finalize()方法是對象逃脫死亡命運的最後一次機會,只要在finalize()中重新與引用鏈上的任何一個對象建立關聯即可。否則稍後GC將對F-Queue中的對象進行第二次小規模的標記。如果對象這時候還沒有逃脫,基本上就會被回收了。

public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVEHOOK = null;

	public void isAlive() {
		System.out.println("alive!");
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize method executed!");
		FinalizeEscapeGC.SAVEHOOK = this;// 在結束的時候將當前對象重新與引用鏈掛鉤,可阻止對象被回收
	}

	public static void main(String[] args) throws InterruptedException {
		SAVEHOOK = new FinalizeEscapeGC();
		SAVEHOOK = null;// 引用制空,告訴GC對象可回收
		System.gc();
		// finalize方法執行優先級較低,虛暫停一會等其執行
		Thread.sleep(500);
		if (SAVEHOOK != null) {
			SAVEHOOK.isAlive();
		} else {
			System.out.println("died!");
		}

		// 第二次自救(不會再執行finalize()方法),自救失敗
		SAVEHOOK = null;// 引用制空,告訴GC對象可回收
		System.gc();
		// finalize方法執行優先級較低,虛暫停一會等其執行
		Thread.sleep(500);
		if (SAVEHOOK != null) {
			SAVEHOOK.isAlive();
		} else {
			System.out.println("died!");
		}
	}
}
代碼中兩次進行對象回收,第一次對象成功逃脫,第二次則失敗被回收。這是因為任何一個對象的finalize()方法都只會被系統自動調用一次。
完全不建議這麽使用!


5.方法區回收(永久代)

在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。

永久代的垃圾回收主要回收 廢棄常量無用的類。如:一個字符串"abc"已經進入了常量池中,但是當前系統中沒有任何一個String對象引用常量池中的"abc"常量,也沒有其他地方引用了這個字面量,就可以被回收。常量池中的其他類(接口),方法,字段的符號引用也與此類似。


無用的類回收條件要苛刻許多:

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
虛擬機對滿足上述3個條件的無用類可以進行回收,但不是必然回收。 是否對類進行回收,HotSpot提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看類加載和卸載信息
在大量使用反射,動態代理,CGLib等ByteCode框架,動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景中需要虛擬機具備類卸載的功能。

2.垃圾收集算法

1.標記-清除算法

分標記和清除兩個階段:首先標記所有需要回收的對象,在標記完成後統一回收所有被標記的對象。 不足:
  • 效率問題:標記和清除兩個過程的效率都不高
  • 空間問題:標記清除後會產生大量不連續的內存碎片,空間碎片太多可能導致以後分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾回收。

2.復制算法

主要用來回收新生代。

解決效率問題。將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這塊內存用完了,就將還存活的對象復制到另一塊上面,然後將已使用過的內存空間一次清理掉。每次都是對整個半區進行內存回收,內存分配時就不用考慮內存碎片等復雜情況,只需要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。代價就是將內存縮小為了原來的一半。


實際引用中並不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性復制到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor大小比例是8:1。即每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被"浪費"。 當保存對象的Survivor空間不夠用時,需要依賴其他內存(老年代)進行分配擔保(Handle Promotion)。


如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。


3.標記-整理算法

復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。老年代一般不能直接選用這種算法。

根據老年代的特點,提出了"標記-整理"(Mark-Compact)算法,標記過程不變,後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。


4.分代收集算法(Generational Collection)

當前商業虛擬機的垃圾收集都采用"分代收集",根據對象存活周期的不同將內存劃分為幾塊。一般將Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用適當的收集算法 。新生代就選用 復制算法,老年代就使用"標記-清除"或"標記-整理"算法進行回收。

3.hotspot的算法實現

1.枚舉根節點

以GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全局性的引用(常量或類靜態屬性)與執行上下文(棧幀中的本地變量表)中。 可達性分析為了確保分析結果的準確性,分析工作必須在一個能確保一致性的快照中進行。這就導致GC進行時必須停頓所有Java執行線程。
如何快速獲取上下文和全局引用信息 目前的主流Java虛擬機使用的都是準確式GC,當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機有辦法直接得知哪些地方存放這對象引用。 在HotSpot的實現中,是使用一組稱為OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內各個偏移量上的各種類型的數據計算出來了,在JIT編譯過程總,也會在特定位置記錄下棧和寄存器中哪些位置是引用。因此GC在掃描時就可以直接得知這些信息了。

2.安全點

在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉。但問題是如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。
實際上HotSpot並沒有為每條指令都生成OopMap,而只是在特定位置記錄這些信息,稱為安全點(Safepoint)。程序執行時並非在所有地方都能停下來開始GC,只有在到達安全點時才能暫停。Safepoint選定既不能太少以致於讓GC等待時間太長,也不能太頻繁而過分增大運行時的負荷。
安全點的選定:以程序"是否具有讓程序長時間執行的特征"為標準進行選定。
“長時間執行”的指令一般是指令序列復用,如:方法調用,循環跳轉,異常跳轉。具有這些功能的指令才會產生Safepoint。
如何在GC發生時讓所有線程都跑到最近的安全點上再停頓下來?
1.搶先式中斷(Preemptive Suspension)
GC發生時,先將所有線程中斷,如果線程中斷的地方不在安全點,就恢復線程,讓其執行到安全點。(幾乎沒有虛擬機使用)

2.主動式中斷(Voluntary Suspension)

當GC需要中斷線程的時候,不直接對線程操作,只是簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起,輪詢標誌和安全點是重合的,另外再加上創建對象需要分配內存的地方。

3.安全區域(Safe Region)

Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。 如果程序不在執行的時候怎麽處理?

如果線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求(執行到安全地方中斷掛起),JVM也不太可能等待線程重新被分配CPU時間。

安全區域:指在一段代碼片段中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的,我們也可以把SafeRegion看成Safepoint的擴展。


在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那麽,當在這段時間裏JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,線程就繼續執行,否則就必須等待直到收到可以安全離開Safe Region的信號為止。

4.垃圾收集器

Java內存回收的具體實現。HosSpot虛擬機實現:

1.serial收集器

Serial是最基本,發展歷史最悠久的收集器(新生代收集器)。是一個單線程的收集器,在它進行垃圾收集時,必須暫停其他所有的工作線程(Stop The World)。這項工作實際上是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。
優點:簡單高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇。

2.parnew收集器

就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為與Serial收集器一樣。如:控制參數(-XX:SurvivorRatio,-XX:PretenureSizeThreshold,-XX:HandlePromotionFailure等),收集算法,Stop The World,對象分配規則,回收策略等。
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。有一個與性能無關但很重要的原因是,除了Serial收集器外,只有它能與CMS收集器配合工作。 ParNew收集器也是使用 -XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。 默認開啟的收集線程數與CPU的數量相同,在CPU非常多的情況下可以通過 -XX:ParallelGCThreads參數來限制垃圾收集的線程數。
並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。 並發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。

3.parallel scavenge收集器

新生代收集器,使用復制算法,並行的多線程收集器。 Parallel  Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。 吞吐量:CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,垃圾收集1分鐘,吞吐量就是99%. 高吞吐量可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。
-XX:MaxGCPauseMillis:控制最大垃圾回收停頓的時間。值為大於0的毫秒數
收集器將盡可能地保證內存回收花費的時間不超過設定值。並不是參數越小就能使垃圾回收更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。停頓時間縮短反而增加垃圾回收的頻率。
-XX:GCTimeRatio:設置吞吐量大小。值為大於0小於100的整數,就是垃圾回收時間占總時間的比率(吞吐量的倒數)。

如果參數設置為19,允許的最大GC時間就占總時間的5%(即,1/(1+19)),默認是99,允許最大1%(1/(1+99))的垃圾回收時間


Parallel Scavenge收集器也稱為"吞吐量優先"收集器

-XX:+UseAdaptiveSizePolicy:這是一個開關參數,不需要手工指定新生代的大小(-Xmn),Eden與Survivor區的比例(-XX:SurvivorRation),晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,稱為GC自適應的調節策略(GC Ergonomics)。


如果手工優化存在困難時,可以使用自適應調節策略。只需設置基本參數(如-Xmx),然後使用MaxGCPauseMillis或GCTimeRatio參數給虛擬機設立一個優化目標,具體細節參數調節工作交給虛擬機。

4.serial old收集器

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

5.parallel old收集器

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

6.cms收集器(Concurrent Mark Sweep)

老年代收集器,以獲取最短回收停頓時間為目標的收集器。適合重視服務響應速度,希望停頓時間短的應用。是基於“標記-清除”算法實現的。 整體運作過程分一下4步:
  • 初始標記(CMS initial mark)
"Stop The World",標記下GC Roots能直接關聯到的對象。
  • 並發標記(CMS concurrent mark)
進行GC Roots Tracing的過程
  • 重新標記(CMS remark)
"Stop The World",修正並發標記期間因用戶程序繼續運作而導致標記產生變動的哪一部分對象的標記記錄,這個階段停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
  • 並發清除(CMS concurrent  sweep)

由於整個過程中耗時最長的並發標記和並發清除過程都可以和用戶線程一起工作,所以總體上說,CMS收集器的內存回收過程是與用戶線程一起並發執行的。 缺點:
  • CMS收集器對CPU資源非常敏感。其實,面向並發設計的程序都對CPU資源比較敏感,在並發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數是(CPU數量+3)/4,即CPU在4個以上時,並發回收線程不少於25%的資源。
  • 浮動垃圾(Floating Garbage):CMS並發清理階段,用戶線程新產生的垃圾,CMS無法立刻處理,留待下一次GC時再清理。
CMS收集器由於無法處理浮動垃圾,可能出現"Concurrent Mode Faile"失敗而導致另一次Full GC的產生。由於垃圾回收時用戶線程還在運行,那麽就還需要預留內存空間給用戶線程使用,因此CMS不能像其他收集器那樣等到老年代幾乎完全被填滿了再回收。 如果CMS運行期間預留的內存無法滿足程序需要,就會出現"Concurrent Mode Failure"失敗。 JDK1.6中,老年代使用92%後會啟動回收(-XX:CMSInitiatingOccupancyFraction 參數進行設置)。
  • CMS基於“標記--清除”算法實現,回收結束會產生大量內存碎片,碎片過多時,會給大對象分配照成影響。現象:老年代還有很大空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。
-XX:UseCMSCompactAtFullCollection 開關參數(默認開啟),在CMS收集器頂不住要FullGC時開啟內存碎片合並整理過程,由於內存整理過程無法並發,停頓時間就會變長 -XX:CMSFullGCsBeforeCompaction,設置每隔多少次Full GC,進行一次壓縮(默認為0,表示每次都進行整理)。

7.g1收集器(Garbage-First)

G1是一款面向服務端應用的垃圾收集器,特點如下:
  • 並行與並發:G1能充分多CPU,多核環境下的硬件優勢,使用多CPU來縮短Stop-The-World停頓時間。
  • 分代收集:G1可以獨立管理整個GC堆,能夠采用不同方式去處理新創建的對象和已經存活了一段時間,熬過多次GC的舊對象以獲取更好的回收效果。
  • 空間整合:G1從整體看是基於"標記--整理",從局部(兩個Region之間)上來看,是基於“復制”算法實現的。從而能夠保證G1運行期間不會產生內存碎片。
  • 可預測的停頓:G1相對於CMS的另一大優勢,G1建立了可預測的停頓時間模型,讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
將整個Java堆劃分為多個大小相等的區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離了,都是Region(不一定連續)的集合。 G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(Garbage-First名稱由來)。這種方式保證了G1收集器在有限的時間內可以獲取盡可能高的回收效率。 問題:
一個對象分配在某個Region中,並非只能被本Region中的對象引用,而是可以與整個Java堆任意的對象發生引用。那麽在做可達性判斷對象是否存活的時候,就得掃描整個Java堆。

解決:

Region之間的對象引用以及其他收集器新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都對應一個Remembered Set。當虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之中(分代例子中就是檢查是否對象跨代引用了),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remember Set之中。當進行回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。

G1操作步驟:

  • 初始標記(Initial Marking)

標記GC Roots能直接關聯到的對象,並修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可用的Region中創建新對象,需要停頓線程,耗時短。

  • 並發標記(Concurrent Marking)

從GC Root開始對堆中對象進行可達性分析,找出存活的對象,耗時較長,可與用戶程序並發執行。

  • 最終標記(Final Marking)

修正在並發標記期間用戶進程繼續運作導致的引用變動,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,然後將Remembered Set Logs的數據合並到Remembered Set中,這階段需要停頓線程,但是可並行執行

  • 篩選回收(Live Data Counting and Evacuation)

對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來指定回收計劃

8.gc日誌

每種收集器的日誌形式都由自身的實現所決定,格式都可以不一樣。但是虛擬機將各個收集器的日誌到維持了一定的共性。


9.垃圾收集器參數總結

  •  UseSerialGC  : 虛擬機運行在Client模式下的默認值,打開此開關後,Serial+Serial Old的收集器組合進行內存回收。
  •  UseParNewGC : 打開此開關後,使用ParNew+Serial Old 的收集器組合進行內存回收。
  •  UseConcMarkSweepGC  :使用ParNew+CMS+Serial Old的收集器組合進行內存回收。Serial Old作為CMS出現Concurrent Mode Failure失敗後的備用收集器。
  •  UseParallelGC  : 虛擬機運行在Server模式下的默認值,使用Parallel Scavenge + Serial Old (PS MarkSweep)的組合進行回收。
  •  UseParallelOldGC  : 使用Parallel Scavenge+Parallel Old的收集器組合進行內存回收
  •  SurvivorRation  : 新生代中Eden區域與Survivor區域的容量比值,默認8
  •  PretenureSizeThreshold  : 直接晉升到老年代的對象大小,設置後,大於這個參數的對象將直接在老年代分配。
  •  MaxTenuringThreshold  : 晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC(頻繁發生在新生代的GC)後,年齡就+1,當超過這個參數時就進入老年代。
  •  UseAdaptiveSizePolicy  : 動態調整Java堆中各個區域的大小以及進入老年代的年齡
  •  HandlePromotionFailure  : 是否允許分配擔保失敗--老年代剩余空間無法滿足 Eden和Survivor區對象存活太多的情況。
  •  ParallelGCThreads  : 設置並行GC時進行內存回收的線程數。
  •  GCTimeRatio  : GC時間占總時間的比率,默認99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效。
  •  MaxGCPauseMillis  : 設置GC最大停頓時間,僅在使用Parallel Scavenge收集器時生效
  •  CMSInitiatingOccupancyFraction  : 設置CMS收集器在老年代空間被是使用多少後觸發垃圾回收。默認68%。僅在使用CMS收集器時有效
  •  UseCMSCompactAtFullCollection  : 設置CMS收集器在完成垃圾回收後是否進行內存整理
  •  CMSFullGCsBeforeCompaction  : CMS收集器在進行多少次回收後進行一次內存碎片整理。

5.內存分配與回收策略

Java技術體系所提倡的自動內存管理主要解決兩個問題:給對象分配內存和回收分配給對象的內存。
對象內存分配主要是在堆上分配,主要分配在新生代的Eden區,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配。少數情況可能會直接分配在老年代中,分配規則不是固定的。具體取決於使用的哪種垃圾回收器組合,和虛擬機中與內存相關的參數設置。

1.對象優先在eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。 通過 -XX:+PrintGCDetails 打印回收日期,進行查看。
	/**
	 * VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
	 * -XX:SurvivorRatio=8 新生代大小 Eden與Survivor區大小比
	 */
	public 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];// 出現一次Minor GC
	}
GC日誌:
[GC [DefNew: 6487K->152K(9216K), 0.0066114 secs] 6487K->6296K(19456K), 0.0066516 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4412K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,  52% used [0x33090000, 0x334b8fd8, 0x33890000)
  from space 1024K,  14% used [0x33990000, 0x339b6270, 0x33a90000)
  to   space 1024K,   0% used [0x33890000, 0x33890000, 0x33990000)
 tenured generation   total 10240K, used 6144K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  60% used [0x33a90000, 0x34090030, 0x34090200, 0x34490000)
 compacting perm gen  total 12288K, used 369K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec460, 0x344ec600, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)
從上面信息可以看出:Eden區是  8192K(8M),from space(survivor1) 1024k(1M),to space(survivor2)1024k。
在聲明前三個變量(allocation1,allocation2,allocation3)時,占用Eden區6M內存,在聲明allocation4時,剩余的空間不足4MB內存,所以發生了Minor GC。GC期間發現3個2MB大小的對象無法放入Survivor空間(只有1MB大小),只好通過分配擔保機制轉移到老年代(可以看到 DefNew:6487K -> 152K(9216K)的GC日誌信息)。總占用內存8487K ->6296K(19456K)幾乎沒減,因為allocation1,allocation2,allocation3變量都還存活。
GC結束後,給allocation4變量分配內存,在後面的堆內存快照中可以看出:Eden區使用了 4412K即4M,老年代10240k(10M)使用了6144k(4M)。
  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,回收速度也比較快
  • 老年代GC(Major GC/Full GC):指發生在老年大的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(非絕對,在Parallel Scavenge回收策略中有直接進行Major GC的策略選擇過程)。Major GC 的速度比 Minor GC慢10倍以上。

2.大對象直接進入老年代

大對象指,需要大量連續內存空間的Java對象,如字符串及數組(例子中的byte[]數組)。經常出現大對象容易導致內存還有不少空間就得提前觸發垃圾回收來獲取足夠的連續空間來進行分配。 -XX:PretenureSizeThreshold 參數,令大於這個設置值的對象直接在老年代分配。避免Eden區及兩個Survivor區之間發生大量的內存復制。
	/**
	 * VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
	 * 			<span style="white-space:pre">		</span>新生代大小 		Eden與Survivor區大小比
	 *  -XX:PretenureSizeThreshold=3145728
	 */
	public static void testPretenureSizeThreshold(){
		byte[] allocation;
		allocation = new byte[4*_1MB]; //直接分配在老年代中
	}
日誌:
Heap
 def new generation   total 9216K, used 507K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,   6% used [0x33090000, 0x3310eee0, 0x33890000)
  from space 1024K,   0% used [0x33890000, 0x33890000, 0x33990000)
  to   space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
 tenured generation   total 10240K, used 4096K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  40% used [0x33a90000, 0x33e90010, 0x33e90200, 0x34490000)
 compacting perm gen  total 12288K, used 369K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec550, 0x344ec600, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)
Eden區幾乎沒有使用,老年代直接使用了4M空間,因為參數的原因,3M以上的對象都會直接在老年代中進行分配。

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

虛擬機采用分代回收的思想來管理內存,那麽就得判斷對象是放在新生代還是老年代。為了做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數器。

如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間,並且對象年齡設為1。此後對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當年齡增加到一定程度(默認15歲),就將會被晉升到老年代中。

-XX:MaxTenuringThreshold   設置年齡閥值。

	/**
	 * VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		    -XX:MaxTenuringThreshold=1
	 * 			-XX:+PrintTenuringDistribution
	 */
	public static void testTenuringThreshold(){
		byte[] allocation1,allocation2,allocation3;
		allocation1 = new byte[_1MB / 4];
		//什麽時候進入老年代取決於XX:MaxTenuringThreshold設置
		allocation2= new byte[4*_1MB];
		allocation3 = new byte[4*_1MB];
		allocation3 = null;
		allocation3 = new byte[4*_1MB];
	}
日誌:


4.動態對象年齡判定

虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

	/**
	 * VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		    -XX:MaxTenuringThreshold=15
	 * 			-XX:+PrintTenuringDistribution
	 */
	public static void testTenuringThreshold2(){
		// allocation1 + allocation2 > Survivor/2
		byte[] allocation1 = new byte[_1MB / 4];
		byte[] allocation2 = new byte[_1MB / 4];
		
		byte[] allocation3 = new byte[_1MB * 4];
		allocation3 =null;
		allocation3 = new byte[_1MB * 4];
		byte[] allocation4 = new byte[_1MB * 4];
		
	}
日誌:


5.空間分配擔保

在Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麽Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置是否允許擔保失敗。如果允許,就繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,盡管Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。

風險:

新生代使用復制收集算法,當Minor GC後仍然存活大量對象,Survivor無法容納時,就需要老年代進行分配擔保,讓對象直接進入老年代。


前提條件: 老年代本身有剩余空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以需要取之前每次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。


取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高於平均值,就會導致擔保失敗(Handle Promotion Failure)。雖然擔保失敗會繞大圈,但大部分情況下還是會開啟HandlePromotionFailure,避免頻繁Full  GC。

	/**
	 * VM 參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * 		   -XX:-HandlePromotionFailure   不開啟
	 * 		   -XX:+HandlePromotionFailure   開啟
	 */
	public static void testHandlePromotion(){
		byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
		allocation1 = new byte[2*_1MB];
		allocation2 = new byte[2*_1MB];
		allocation3 = new byte[2*_1MB];
		allocation1 = null;
		allocation4 = new byte[2*_1MB];
		allocation5 = new byte[2*_1MB];
		allocation6 = new byte[2*_1MB];
		allocation4 = null;
		allocation5 = null;
		allocation6 = null;
		allocation7 = new byte[2*_1MB];
	}
不開啟分配擔保,日誌:
[GC [DefNew: 6487K->152K(9216K), 0.0043054 secs] 6487K->4248K(19456K), 0.0043431 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6382K->6382K(9216K), 0.0000188 secs][Tenured: 4096K->4248K(10240K), 0.0085509 secs] 10478K->4248K(19456K), [Perm : 370K->370K(12288K)], 0.0086249 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
//Full GC
Heap
 def new generation   total 9216K, used 2211K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,  27% used [0x33090000, 0x332b8fd8, 0x33890000)
  from space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
  to   space 1024K,   0% used [0x33890000, 0x33890000, 0x33990000)
 tenured generation   total 10240K, used 4248K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  41% used [0x33a90000, 0x33eb6200, 0x33eb6200, 0x34490000)
 compacting perm gen  total 12288K, used 370K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec8c8, 0x344eca00, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)
開啟分配擔保,日誌:
[GC [DefNew: 6487K->152K(9216K), 0.0043572 secs] 6487K->4248K(19456K), 0.0043931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6382K->152K(9216K), 0.0006780 secs] 10478K->4248K(19456K), 0.0007148 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //Minor GC
Heap
 def new generation   total 9216K, used 2364K [0x33090000, 0x33a90000, 0x33a90000)
  eden space 8192K,  27% used [0x33090000, 0x332b8fd8, 0x33890000)
  from space 1024K,  14% used [0x33890000, 0x338b61e0, 0x33990000)
  to   space 1024K,   0% used [0x33990000, 0x33990000, 0x33a90000)
 tenured generation   total 10240K, used 4096K [0x33a90000, 0x34490000, 0x34490000)
   the space 10240K,  40% used [0x33a90000, 0x33e90020, 0x33e90200, 0x34490000)
 compacting perm gen  total 12288K, used 370K [0x34490000, 0x35090000, 0x38490000)
   the space 12288K,   3% used [0x34490000, 0x344ec8c8, 0x344eca00, 0x35090000)
    ro space 10240K,  54% used [0x38490000, 0x38a0c0f0, 0x38a0c200, 0x38e90000)
    rw space 12288K,  55% used [0x38e90000, 0x3952fb80, 0x3952fc00, 0x39a90000)

JDK6 update 24之後,只要老年代連續空間大於新生代對象總大小或歷次晉升平均大小,就會進行Minor GC,否則進行Full GC。


Tags: private Object public 虛擬機 計數器

文章來源:


ads
ads

相關文章
ads

相關文章

ad