GC之一--GC 的算法分析、垃圾收集器、內存分配策略介紹

 

目錄:

GC之一--GC 的算法分析、垃圾收集器、內存分配策略介紹html

GC之二--GC日誌分析(jdk1.8)整理中java

GC之三--GC 觸發Full GC執行的狀況及應對策略程序員

gc之四--Minor GC、Major GC和Full GC之間的區別算法

GC之六--SystemGC徹底解讀windows

1、概述

垃圾收集 Garbage Collection 一般被稱爲「GC」,它誕生於1960年 MIT 的 Lisp 語言,通過半個多世紀,目前已經十分紅熟了。數組

jvm 中,程序計數器、虛擬機棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨着方法的進入和退出作入棧和出棧操做,實現了自動的內存清理,所以,咱們的內存垃圾回收主要集中於 java 堆和方法區中,在程序運行期間,這部份內存的分配和使用都是動態的.緩存

1.一、執行回收時機

Java的垃圾回收機制是Java虛擬機提供的能力,用於在空閒時間以不定時的方式動態回收無任何引用的對象佔據的內存空間。服務器

須要注意的是:垃圾回收回收的是無任何引用的對象佔據的內存空間而不是對象自己,不少人回答的含義是回收對象,實際上這是不正確的。多線程

System.gc();
Runtime.getRuntime().gc() ;

上面的方法調用時用於顯式通知JVM能夠進行一次垃圾回收,但真正垃圾回收機制具體在什麼時間點開始發生動做這一樣是不可預料的,這和搶佔式的線程在發生做用時的原理同樣。併發

程序員只能經過上面的方法建議JVM回收垃圾,可是JVM是否回收,一樣是不可預料的。

2、對象存活判斷

判斷對象是否存活通常有兩種方式:

引用計數:每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收。此方法簡單,沒法解決對象相互循環引用的問題。

根搜索算法/可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。不可達對象。在Java語言中,GC Roots包括:

  • 虛擬機棧中引用的對象。
  • 方法區中類靜態屬性實體引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI引用的對象

示例1:根據gc結果判斷JVM採用何種方式來判斷對象存活

package com.jvm.study.part3;

import java.util.concurrent.TimeUnit;

/**
 * @VM args:-verbose:gc -XX:+PrintGCDetails
 * @author 01107252
 *
 */
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.out.println("1");
        System.gc();
        System.out.println("2");
        objA = null;
        objB = null;
        System.gc();
        System.out.println("3");
    }
    public static void main(String[] args) throws InterruptedException {
        //TimeUnit.SECONDS.sleep(30);
        testGC();
        System.out.println("4");

    }

}

結果:

[GC (System.gc()) [PSYoungGen: 6000K->4696K(36864K)] 6000K->4704K(121856K), 0.0027438 secs] [Times: user=0.05 sys=0.02, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 4696K->0K(36864K)] [ParOldGen: 8K->4609K(84992K)] 4704K->4609K(121856K), [Metaspace: 2535K->2535K(1056768K)], 0.0075679 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (System.gc()) [PSYoungGen: 634K->64K(36864K)] 5244K->4673K(121856K), 0.0003509 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 64K->0K(36864K)] [ParOldGen: 4609K->513K(84992K)] 4673K->513K(121856K), [Metaspace: 2535K->2535K(1056768K)], 0.0063831 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
4
Heap
 PSYoungGen      total 36864K, used 1270K [0x00000000d7000000, 0x00000000d9900000, 0x0000000100000000)
  eden space 31744K, 4% used [0x00000000d7000000,0x00000000d713d890,0x00000000d8f00000)
  from space 5120K, 0% used [0x00000000d9400000,0x00000000d9400000,0x00000000d9900000)
  to   space 5120K, 0% used [0x00000000d8f00000,0x00000000d8f00000,0x00000000d9400000)
 ParOldGen       total 84992K, used 513K [0x0000000085000000, 0x000000008a300000, 0x00000000d7000000)
  object space 84992K, 0% used [0x0000000085000000,0x0000000085080450,0x000000008a300000)
 Metaspace       used 2541K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 272K, capacity 386K, committed 512K, reserved 1048576K

結果分析:示例中objA和objB存在相互引用,如果採用「引用計數」的方式,理論上不能被回收。但結果可見是被釋放了。側面應正了JVM採用的是非「引用計數」的方式。

示例2:可達性分析

3、再談引用

  不管是經過引用計數算法判斷對象的引用數量,仍是經過根搜索算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在JDK 1.2以前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹,可是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力。咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。

在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。

  1. 強引用:強引用就是指在程序代碼之中廣泛存在的,相似「Object obj = new Object()」這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
  2. 軟引用:軟引用用來描述一些還有用,但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中並進行第二次回收。若是此次回收仍是沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了SoftReference類來實現軟引用。
  3. 弱引用:弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了WeakReference類來實現弱引用。
  4. 虛引用:虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是但願能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了PhantomReference類來實現虛引用。

  參加《對象的強、軟、弱和虛引用

3、生存仍是死亡?

  在根搜索算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。

  若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條由虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的狀況),將極可能會致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至致使整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身—只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那它就真的離死不遠了。從代碼清單3-2中咱們能夠看到一個對象的finalize()被執行,可是它仍然能夠存活。

代碼清單3-2 一次對象自我拯救的演示

/**
* 此代碼演示了兩點:
* 1.對象能夠在被GC時自我拯救。
* 2.這種自救的機會只有一次,由於一個對象的finalize
()方法最多隻會被系統自動調用一次
* @author zzm
*/
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 mehtod executed!");
 FinalizeEscapeGC.SAVE_HOOK = this;
}

public static void main(String[] args) throws Throwable {
 SAVE_HOOK = new FinalizeEscapeGC();

 //對象第一次成功拯救本身
 SAVE_HOOK = null;
 System.gc();
 // 由於Finalizer方法優先級很低,暫停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();
 // 由於Finalizer方法優先級很低,暫停0.5秒,以等待它
 Thread.sleep(500);
 if (SAVE_HOOK != null) {
 SAVE_HOOK.isAlive();
 } else {
 System.out.println("no, i am dead :(");
 }
}
}
運行結果:

finalize mehtod executed!
yes, i am still alive :)
no, i am dead :(
從代碼清單3-2的運行結果能夠看到,SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,而且在被收集前成功逃脫了。

另一個值得注意的地方就是,代碼中有兩段徹底同樣的代碼片斷,執行結果倒是一次逃脫成功,一次失敗,這是由於任何一個對象的finalize()方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize()方法不會被再次執行,所以第二段代碼的自救行動失敗了。

須要特別說明的是,上面關於對象死亡時finalize()方法的描述可能帶有悲情的藝術色彩,筆者並不鼓勵你們使用這種方法來拯救對象。相反,筆者建議你們儘可能避免使用它,由於它不是C/C++中的析構函數,而是Java剛誕生時爲了使C/C++程序員更容易接受它所作出的一個妥協。它的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。有些教材中提到它適合作「關閉外部資源」之類的工做,這徹底是對這種方法的用途的一種自我安慰。finalize()能作的全部工做,使用try-finally或其餘方式均可以作得更好、更及時,你們徹底能夠忘掉Java語言中還有這個方法的存在。

4、回收

4.一、回收方法區

  不少人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集,並且在方法區進行垃圾收集的「性價比」通常比較低:在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。

永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是相似。以常量池中字面量的回收爲例,假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作「abc」的,換句話說是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是在這時候發生內存回收,並且必要的話,這個「abc」常量就會被系統「請」出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。

斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」:

  1. 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而不是和對象同樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數(-Xnoclassgc 表示不對方法區進行垃圾回收。請謹慎使用)進行控制,還可使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看類的加載和卸載信息。

在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。

4.二、棧內存的釋放

  當在一段代碼塊定義一個變量時,Java在棧中爲這個變量分配內存空間,當該變量退出其做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。

4.三、堆(Heap)內存回收--本文關注的重點

  GC爲了可以正確釋放對象,會監控每一個對象的運行情況,對他們的申請、引用、被引用、賦值等情況進行監控,Java會使用有向圖的方法進行管理內存,實時監控對象是否能夠達到,若是不可到達,則就將其回收,這樣也能夠消除引用循環的問題。在Java語言中,判斷一個內存空間是否符合垃圾收集標準有兩個:一個是給對象賦予了空值null,如下再沒有調用過,另外一個是給對象賦予了新值,這樣從新分配了內存空間。

5、垃圾收集算法(內存回收的方法論

5.一、標記-清除算法

  「標記-清除」(Mark-Sweep)算法,如它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其缺點進行改進而獲得的。

它的主要缺點有兩個:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

5.二、複製算法

  「複製」(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。

這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則致使效率下降。

5.三、標記-壓縮算法(標記-整理)

  複製收集算法在對象存活率較高時就要執行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。

根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存

5.四、分代收集算法

GC分代的基本假設:絕大部分對象的生命週期都很是短暫,存活時間短。

「分代收集」(Generational Collection)算法,把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收。

6、垃圾收集器(內存回收的具體實現

垃圾收集器功能總覽

若是說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現。

上圖說明:

新生代:Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1收集器

老年代:CMS(Concurrent Mark Sweep)收集器、Serial Old(MSC)收集器、Parallel Old收集器、G1收集器

收集器之間的鏈接線的意思是:垃圾收集器之間能夠配合工做

6.一、Serial收集器(新生代的垃圾收集器)

  串行收集器是一個新生代收集器,是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收。

收集器配合關係:能夠與CMS收集器、Serial Old收集器配合工做。

採用算法:複製算法

參數控制:-XX:+UseSerialGC 串行收集器

 

6.二、ParNew收集器(新生代的垃圾收集器)

ParNew收集器是一個新生代收集器,其實就是Serial收集器的多線程版本。

收集器配合關係:能夠與CMS收集器、Serial Old收集器配合工做。

採用算法:複製算法

參數控制:-XX:+UseParNewGC ParNew收集器

       -XX:ParallelGCThreads 限制線程數量

 

6.三、Parallel Scavenge(清除)收集器(新生代的垃圾收集器)

  Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器……看上去和ParNew都同樣,那它有什麼特別之處呢?

  Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

響應時間:停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。

高吞吐量:而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

MaxGCPauseMillis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超過設定值。不過你們不要認爲若是把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代確定比收集500MB快吧,這也直接致使垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,如今變成5秒收集一次、每次停頓70毫秒。停頓時間的確在降低,但吞吐量也降下來了。

GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,至關因而吞吐量的倒數。若是把此參數設置爲19,那容許的最大GC時間就佔總時間的5%(即1 /(1+19)),默認值爲99,就是容許最大1%(即1 /(1+99))的垃圾收集時間。

因爲與吞吐量關係密切,Parallel Scavenge收集器也常常稱爲「吞吐量優先」收集器。除上述兩個參數以外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開以後,就不須要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)。若是讀者對於收集器運做原來不太瞭解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。只須要把基本的內存數據設置好(如-Xmx設置最大堆),而後使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標,那具體細節參數的調節工做就由虛擬機完成了。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

參數控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

====================================以上都是運行在新生代的垃圾回收器====================================

====================================接下來的是老年代垃圾回收器=========================================

6.四、Serial Old收集器

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

採用算法:複製算法

Serial Old收集器的工做過程如圖3-8所示。

6.六、Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器是在JDK 1.6中才開始提供的,在此以前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。緣由是,若是新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器沒法與CMS收集器配合工做嗎?)。因爲老年代Serial Old收集器在服務端應用性能上的「拖累」,使用了Parallel Scavenge收集器也未必能在總體應用上得到吞吐量最大化的效果,因爲單線程的老年代收集中沒法充分利用服務器多CPU的處理能力,在老年代很大並且硬件比較高級的環境中,這種組合的吞吐量甚至還不必定有ParNew加CMS的組合「給力」。

直到Parallel Old收集器出現後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工做過程如圖3-9所示。

參數控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代並行

6.七、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。

從名字(包含「Mark Sweep」)上就能夠看出CMS收集器是基於「標記-清除」算法實現的,它的運做過程相對於前面幾種收集器來講要更復雜一些,整個過程分爲4個步驟,包括:

初始標記(CMS initial mark)

併發標記(CMS concurrent mark)

從新標記(CMS remark)

併發清除(CMS concurrent sweep)

其中初始標記、從新標記這兩個步驟仍然須要「Stop The World」。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而從新標記階段則是爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,因此整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發地執行。老年代收集器(新生代使用ParNew)

優勢:併發收集、低停頓

缺點:產生大量空間碎片、併發階段會下降吞吐量

參數控制:-XX:+UseConcMarkSweepGC 使用CMS收集器

-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次碎片整理;整理過程是獨佔的,會引發停頓時間變長

-XX:+CMSFullGCsBeforeCompaction 設置進行幾回Full GC後,進行一次碎片整理

-XX:ParallelCMSThreads 設定CMS的線程數量(通常狀況約等於可用CPU數量)

 

6.八、G1(Garbage-First)收集器

G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,早在JDK 1.7剛剛確立項目目標,Sun公司給出的JDK 1.7 RoadMap裏面,它就被視爲JDK 1.7中HotSpot虛擬機的一個重要進化特徵。從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的「Experimental」狀態持續了數年時間,直至JDK 7u4,Sun公司才認爲它達到足夠成熟的商用程度,移除了「Experimental」的標識。

G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK1.5中發佈的CMS收集器。與CMS收集器相比G1收集器有如下特色:

1. 空間整合,G1收集器採用標記整理算法,不會產生內存空間碎片。分配大對象時不會由於沒法找到連續空間而提早觸發下一次GC。

2. 可預測停頓,這是G1的另外一大優點,下降停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲N毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔閡了,它們都是一部分(能夠不連續)Region的集合。

G1的新生代收集跟ParNew相似,當新生代佔用達到必定比例的時候,開始出發收集。和CMS相似,G1收集器收集老年代對象會有短暫停頓。

 

收集步驟

一、標記階段,首先初始標記(Initial-Mark),這個階段是停頓的(Stop the World Event),而且會觸發一次普通Mintor GC。對應GC log:GC pause (young) (inital-mark)

二、Root Region Scanning,程序運行過程當中會回收survivor區(存活到老年代),這一過程必須在young GC以前完成。

三、Concurrent Marking,在整個堆中進行併發標記(和應用程序併發執行),此過程可能被young GC中斷。在併發標記階段,若發現區域對象中的全部對象都是垃圾,那個這個區域會被當即回收(圖中打X)。同時,併發標記過程當中,會計算每一個區域的對象活性(區域中存活對象的比例)。

四、Remark, 再標記,會有短暫停頓(STW)。再標記階段是用來收集 併發標記階段 產生新的垃圾(併發階段和應用程序一同運行);G1中採用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

五、Copy/Clean up,多線程清除失活對象,會有STW。G1將回收區域的存活對象拷貝到新區域,清除Remember Sets,併發清空回收區域並把它返回到空閒區域鏈表中。

六、複製/清除過程後。回收區域的活性對象已經被集中回收到深藍色和深綠色區域。

 

7、經常使用的收集器組合

7.一、 新生代和老年代的收集器組合對應表

 參數

新生代GC策略

年老代GC策略

說明

UseSerialGC

Serial

Serial Old

SerialSerial Old都是單線程進行GC,特色就是GC時暫停全部應用線程。

UseSerialGC(組合2)

Serial

CMS+Serial Old

CMSConcurrent Mark Sweep)是併發GC,實現GC線程和應用線程併發工做,不須要暫停全部應用線程。另外,當CMS進行GC失敗時,會自動使用Serial Old策略進行GC

UseConcMarkSweepGC

ParNew

CMS

使用-XX:+UseParNewGC選項來開啓。ParNewSerial的並行版本,能夠指定GC線程數,默認GC線程數爲CPU的數量。可使用-XX:ParallelGCThreads選項指定GC的線程數。

若是指定了選項-XX:+UseConcMarkSweepGC選項,則新生代默認使用ParNew GC策略。

UseParNewGC

ParNew

Serial Old

使用-XX:+UseParNewGC選項來開啓。新生代使用ParNew GC策略,年老代默認使用Serial Old GC策略。

UseParallelGC

Parallel Scavenge

Serial Old

Parallel Scavenge策略主要是關注一個可控的吞吐量:應用程序運行時間 / (應用程序運行時間 + GC時間),可見這會使得CPU的利用率儘量的高,適用於後臺持久運行的應用程序,而不適用於交互較多的應用程序。

UseParallelOldGC

Parallel Scavenge

Parallel Old

Parallel OldSerial Old的並行版本

 

-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC

G1GC

G1GC

-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC        #開啓
-XX:MaxGCPauseMillis =50                  #暫停時間目標
-XX:GCPauseIntervalMillis =200          #暫停間隔目標
-XX:+G1YoungGenSize=512m            #年輕代大小
-XX:SurvivorRatio=6                            #倖存區比例

 

7.二、 組合下對應的運行示意圖

 第1個組合:Serial + Serial Old

 第2個組合:Serial + (CMS + Serial Old)(當CMS進行GC失敗時,會自動使用Serial Old策略進行GC)

 

 第3個組合:ParNew+ Serial Old

 第4個組合:ParNew + CMS

 

 第5個組合:Parallel Scavenge + Parallel Old

 

 

8、JVM內存分配策略

1. 對象優先在Eden分配

若是Eden區不足分配對象,會作一個minor gc,回收內存,嘗試分配對象,若是依然不足分配,才分配到Old區。

 示例:

/** 
     * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC */  
    @SuppressWarnings("unused")  
    public static void testAllocation() {  
        byte[] allocation1, allocation2, allocation3, allocation4;  
        allocation1 = new byte[2 * _1MB];  
        allocation2 = new byte[2 * _1MB]; 
        System.out.println("1");
        allocation3 = new byte[2 * _1MB];  
        System.out.println("2");
        allocation4 = new byte[4 * _1MB];  // 出現一次Minor GC  
        System.out.println("3");
    }  

結果:

1
2
[GC (Allocation Failure) [DefNew: 7127K->523K(9216K), 0.0090101 secs] 7127K->6667K(19456K), 0.0091057 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
3
Heap
 def new generation   total 9216K, used 4862K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff03c988, 0x00000000ff400000)
  from space 1024K,  51% used [0x00000000ff500000, 0x00000000ff582ec0, 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 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

結果分析:

一、jdk8中須要修改默認的收集器,將其修改成串行:-XX:+UseSerialGC

 

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

大對象是指須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組,虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代中分配。這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存拷貝(新生代採用複製算法收集內存)。-XX:PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效

示例:

/**
     * @VM args:-verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8
     * -XX:PretenureSizeThreshold=3145728 -XX:+PrintGCDetails -Xloggc:gc.log -XX:+UseSerialGC
     * 3145728=3m
     * @author lenovo
     */
    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4 * _1MB];
        
    }

結果:

Java HotSpot(TM) 64-Bit Server VM (25.77-b03) for windows-amd64 JRE (1.8.0_77-b03), built on Mar 20 2016 22:01:33 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16199336k(11697456k free), swap 19213992k(13645132k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:PretenureSizeThreshold=3145728 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
Heap
 def new generation   total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed1ef60, 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 2571K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

結果分析:

一、要爲年輕代配置串行收集器,默認爲-XX:+UseParallelGC,不然-XX:PretenureSizeThreshold參數無效的。

二、4m的byte數組直接被分配到老年代,上面gc日誌的紅色標紅部分。

 

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

  在經歷了屢次的Minor GC後仍然存活:在觸發了Minor GC後,存活對象被存入Survivor區在經歷了屢次Minor GC以後,若是仍然存活的話,則該對象被晉升到Old區。
虛擬機既然採用了分代收集的思想來管理內存,那內存回收時就必須能識別哪些對象應當放在新生代,哪些對象應放在老年代中。爲了作到這點,虛擬機給每一個對象定義了一個對象年齡(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:+UseSerialGC -XX:+PrintTenuringDistribution 
     */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold() {  
        byte[] allocation1, allocation2, allocation3;  
        allocation1 = new byte[_1MB / 4];  // 何時進入老年代決定於XX:MaxTenuringThreshold設置
        System.out.println("1");
        allocation2 = new byte[4 * _1MB];
        System.out.println("2");
        allocation3 = new byte[4 * _1MB];
        System.out.println("3");
        allocation3 = null;  
        System.out.println("4");
        allocation3 = new byte[4 * _1MB];
        System.out.println("5");
    } 

結果:

1
2
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     798424 bytes,     798424 total
: 5335K->779K(9216K), 0.0067146 secs] 5335K->4875K(19456K), 0.0068249 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
3
4
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:         96 bytes,         96 total
: 5034K->0K(9216K), 0.0022269 secs] 9130K->4875K(19456K), 0.0022859 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5
Heap
 def new generation   total 9216K, used 4419K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff050ce8, 0x00000000ff400000)
  from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400060, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4874K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffac2bc8, 0x00000000ffac2c00, 0x0000000100000000)
 Metaspace       used 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

爲-XX:MaxTenuringThreshold=15時的結果是:

1
2
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     798424 bytes,     798424 total
: 5335K->779K(9216K), 0.0072603 secs] 5335K->4875K(19456K), 0.0073546 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
3
4
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:         96 bytes,         96 total
: 5034K->0K(9216K), 0.0023141 secs] 9130K->4875K(19456K), 0.0023590 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5
Heap
 def new generation   total 9216K, used 4419K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff050ce8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400060, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4874K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffac2bc8, 0x00000000ffac2c00, 0x0000000100000000)
 Metaspace       used 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

結果分析:

4.動態對象年齡斷定

  爲了能更好地適應不一樣程序的內存情況,虛擬機並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

/** 
     * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
     * -XX:+PrintTenuringDistribution 
     */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold2() {  
        byte[] allocation1, allocation2, allocation3, allocation4;  
        allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大於survivo空間一半  
        System.out.println("1");
        allocation2 = new byte[_1MB / 4];
        System.out.println("2");
        allocation3 = new byte[4 * _1MB];
        System.out.println("3");
        allocation4 = new byte[4 * _1MB];
        System.out.println("4");
        allocation4 = null;  
        System.out.println("5");
        allocation4 = new byte[4 * _1MB];
        System.out.println("6");
    }  

結果:

1
2
3
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1048576 bytes,    1048576 total
: 5591K->1024K(9216K), 0.0085177 secs] 5591K->5131K(19456K), 0.0086216 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
4
5
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:         96 bytes,         96 total
: 5279K->0K(9216K), 0.0024949 secs] 9387K->5131K(19456K), 0.0025430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
6
Heap
 def new generation   total 9216K, used 4419K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff050d88, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400060, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5131K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb02f78, 0x00000000ffb03000, 0x0000000100000000)
 Metaspace       used 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

 

5.Minor GC後Survivor空間不足就直接放入Old區


6.空間分配擔保

  在發生Minor GC時,虛擬機會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC。若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗;若是容許,那隻會進行Minor GC;若是不容許,則也要改成進行一次Full GC。大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。在JDK 6 Update 24以後,這個測試結果會有差別,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化(見代碼清單3-10),雖然源碼中還定義了HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK 6 Update 24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

9、如何監視GC

1.概覽監視gc。

   jmap -heap [pid] 查看內存分佈

   jstat -gcutil [pid] 1000 每隔1s輸出java進程的gc狀況

2.詳細監視gc。

   在jvm啓動參數,加入-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log。

   輸入示例:

 [GC [ParNew: 11450951K->1014116K(11673600K), 0.8698830 secs] 27569972K->17943420K(37614976K), 0.8699520 secs] [Times: user=11.28 sys=0.82, real=0.86 secs]

   表示發生一次minor GC,ParNew是新生代的gc算法,11450951K表示eden區的存活對象的內存總和,1014116K表示回收後的存活對象的內存總和,11673600K是整個eden區的內存總和。0.8699520 secs表示minor gc花費的時間。

   27569972K表示整個heap區的存活對象總和,17943420K表示回收後整個heap區的存活對象總和,37614976K表示整個heap區的內存總和。

[Full GC [Tenured: 27569972K->16569972K(27569972K), 180.2368177 secs] 36614976K->27569972K(37614976K), [Perm : 28671K->28635K(28672K)], 0.2371537 secs]

  表示發生了一次Full GC,整個JVM都停頓了180多秒,輸出說明同上。只是Tenured: 27569972K->16569972K(27569972K)表示的是old區,而上面是eden區。

10、關鍵術語

並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。

吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)

響應時間:停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。

高吞吐量:而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

相關文章
相關標籤/搜索