垃圾收集器

圖片看不清楚,能夠下載或在頁面中單獨查看圖片

1. 概述

Garbage Collection, GC:1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集器技術的語言。java

程序計數器,虛擬機棧,本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的(儘管在運行期會由JIT編譯器進行一些優化),所以這幾個區域的內存分配和回收都具有肯定性,在這幾個區域內就不須要過多考慮回收的問題。git

Java堆和方法區則不同:一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,只能在運行期才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存。github

2. 對象已死嗎?

堆裏面存放着Java中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,須要肯定那些還「存活」,哪些已經「死去」,即不可能再被任何途徑使用的對象。算法

2.1 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。數組

應用:微軟的COM計數,AS3,Python語言等。安全

2.2 可達性分析算法

Reachability Analysis:經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。服務器

 

 對象object5,object6,object7雖然互相有關聯,可是它們到GC Roots是不可達的,因此它們將會被斷定爲可回收的對象。數據結構

 在Java中,可做爲GC Roots的對象包括下面幾種:多線程

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(即通常的Native方法)引用的對象

2.3 強引用,軟引用,弱引用,虛引用

jdk1.2以前,Java中引用的定義:若是reference類型的數據中的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明着一個引用。併發

在jdk1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用,軟引用,弱引用,虛引用。強度依次逐漸減弱。

(1)強引用:程序中廣泛存在的,相似Object obj = new Object(),這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象;

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

(3)弱引用:當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被引用關聯的對象。在jdk1.2以後,提供了WeakReference類來實現弱引用;

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

2.4 finalize()方法

即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程

若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。(1)當對象沒有覆蓋finalize()方法;(2)finalize方法已經被虛擬機調用過;虛擬機這兩種狀況都視爲「沒有必要執行」。

若是對象被斷定爲有必要執行finalize方法,那麼對象將會放置在一個叫作F-Queue的隊列中,虛擬機自動創建一個優先級低的Finalizer線程去執行它(這裏「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象的finlize方法執行緩慢或發生死循環,將極可能致使F-Queue隊列中其餘對象永久處於等待)。稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象從新與引用鏈上的任何對象創建關聯便可,那個第二次標記時它將被移除「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。

實例:

package org.github.oom;

public class FinalizeEscape {
    public static FinalizeEscape fe = null;
    public void alive() {
        System.out.println("yes, i am still alive...");
    }
    public static void dead() {
        System.out.println("no, i am dead...");
    }
    
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!!!");
        fe = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        fe = new FinalizeEscape();
        // 對象第一次成功拯救本身
        fe = null;
        System.gc();
        // 由於finalize方法優先級很低,因此暫停1秒,等待它
        Thread.sleep(1000);
        if (fe != null) {
            fe.alive();
        } else {
            dead();
        }
        
        // 拯救失敗
        fe = null;
        System.gc();
        // 由於finalize方法優先級很低,因此暫停1秒,等待它
        Thread.sleep(1000);
        if (fe != null) {
            fe.alive();
        } else {
            dead();
        }
    }

}  

運行結果:

 

注意,任何一個對象的finalize方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize方法不會被再次執行。

2.5 回收方法區

HotSpot虛擬機中的實現是永久代,主要回收兩個部分:廢棄常量和無用的類。

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

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

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

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

3. 垃圾收集算法

因爲垃圾收集算法的實現涉及大量的程序細節,並且各個平臺的虛擬機操做內存的方法又各不相同,所以不打算過多討論算法的實現。

3.1 標記 - 清除算法

Mark-Sweep算法分爲「標記」和「清除」兩個階段,首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。

它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。

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

 

3.2 複製算法

爲了解決效率問題,一種稱爲「複製」的收集算法,它將可用內存按容量劃分爲大小相等的兩塊。每次只使用其中的一塊。當一塊的內存用完了,就將還存活的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。每次都是對整個半區進行內存回收,內存分配時就不用考慮內存碎片等複雜狀況。

 

如今的商業虛擬機都採用這種收集算法來回收新生代,將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(Eden + 一個Survivor),只有10%會被浪費。當存活對象大於10%,另外一Survivor空間不夠時,須要依賴其餘內存(老年代)進行分配擔保。

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

3.3 標記 - 整理算法

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

3.4 分代收集算法

當前商業虛擬機的垃圾收集都採用「分代收集」算法(Generational Collection)。根據對象存活週期的不一樣將內存劃分爲幾塊,Java堆分爲新生代和老年代,根據各個年代的特色採用最適當的收集算法。

新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。

老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記 - 清理」或「標記 - 整理」算法進行回收。

3.5 HotSpot的算法實現

上面介紹了對象存活斷定算法和垃圾收集算法,而在HotSpot虛擬機上實現這些算法時,必須對算法的執行效率有嚴格的考量,才能保證虛擬機高效運行。

3.5.1 枚舉根節點

從可達性分析中從GC Roots節點找引用鏈這個操做爲例,可做爲GC Roots的節點主要在全局性的引用(常量或類靜態屬性等)與執行上下文(棧幀中的本地變量表)中。

如今不少應用僅僅方法區都有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。

另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行。

目前的虛擬機使用的都是準確式GC,因此當執行系統停頓下來後,並不須要一個不漏地檢查完全部執行上下文和全局的引用位置,HotSpot虛擬機中使用一組稱爲OopMap的數據結構來達到這個目的。

3.5.2 安全點

在OopMap的協助下,HotSpot能夠快速且準確地完成GC Roots枚舉,但一個很現實的問題隨之而來:可能致使引用關係變化,或者說OopMap內容變化的指令很是多,若是爲每一條指令都生成對應的OopMap,將會須要大量的額外空間,這樣GC的空間成本將會變得很高。

實際上,HotSpot也的確沒有爲每條指令都生成OopMap,只有在特定的位置記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並不是在全部地方都能停頓下來開始GC,只有在達到安全點時才能暫停。Safepoint的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的。

「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。

對於安全點,另外一個須要考慮的問題是如何在GC發生時讓全部線程都「跑」到最近的安全點上再停頓下來。有2種方案可供選擇:搶先式中斷和主動式中斷。

(1)搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今JVM沒有采用這種方法。

(2)主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起,輪詢標誌的方法和安全點是重合的,另外再加上建立對象須要分配內存的地方。

3.5.3 安全區域

使用Safepoint彷佛已經完美解決了如何進入GC的問題,但實際狀況卻並不必定。Safepoint機制保障了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。可是,程序「不執行」的時候呢?

所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於sleep狀態或Blocked狀態,這時候程序沒法響應JVM的中斷請求,「走」到安全的地方去中斷掛起,JVM也顯然不太可能等待線程從新被分配CPU時間,對於這種狀況,就須要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的,也能夠把Safe Region看作是被擴展了的Safepoint。

在線程執行到Safe Region中的代碼時,首先標示本身已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不會管標識本身爲Safe Region狀態的線程了。

在線程要離開Safe Region時,它要檢測系統是否已經完成了根節點枚舉(或整個GC過程),若是完成了,那線程就繼續執行,不然它必須等待知道收到能夠安全離開Safe Region的信號爲止。

4. 垃圾收集器

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

Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商、不一樣版本的虛擬機所提供的垃圾收集器均可能會有很大差異,而且通常都會提供參數供用戶根據本身的應用特徵和要求組合出各個年代所使用的收集器。

目前主要有7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。收集器所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。

注:對各個收集器進行比較,但並不是爲了挑選一個最好的收集器,由於直到如今爲止尚未最好的收集器出現,更加沒有萬能的收集器,因此咱們選擇的只是對具體應用最合適的收集器

4.1 Serial收集器

虛擬機運行在Client模式下的默認新生代收集器。只會使用一個CPU或一條收集線程去完成垃圾收集工做。

簡單高效,對於限定單個CPU的環境來講,Serial收集器因爲沒有先交互額開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。

 

4.2 ParNew收集器

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

 

ParNew是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中一個很重要的緣由是除了Serial收集器外,只有它能與CMS收集器配合工做。

備註:此處解釋並行與併發的區別

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

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

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的並行收集器。

Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量:CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間) 

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量:

(1)-XX:MaxGCPauseMillis最大垃圾收集停頓時間,參數容許的值是一個大於0的毫秒數。

不要覺得把這個參數的值設置得小一點就能使得系統的垃圾收集速度變得更快。GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一點,收集300MB新生代確定比收集500MB快,這也直接致使垃圾收集發生得更頻繁一些,原來10秒收集一次,每次停頓100毫秒,如今變成5秒收集一次,每次停頓70毫秒。停頓時間的確實降低了,可是次數多了,吞吐量也就降下來了。

(2)-XX:GCTimeRatio設置吞吐量大小,值應該是一個0 < xx < 100的整數,默認值爲99,就是容許最大1%(即1 /(1 + 99))的垃圾收集時間。

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

4.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,單線程,標記 - 整理算法, 這個收集器主要用在Client模式下。若是是Server模式下,有兩個用途(1)與Parallel Scavenge收集器搭配使用;(2)在CMS併發收集發生Concurrent Mode Failure時做爲CMS收集器的後備預案。

注:Concurrent Mode Failure後面會介紹。

4.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,多線程,標記 - 整理算法。

4.6 CMS收集器

Concurrent Mark Sweep是一種以獲取最短回收停頓時間爲目標的收集器,多線程,標記 - 清除算法(碎片問題)。目前很大一部分的Java應用集中在互聯網或者B/S系統的服務器上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。

它的運行過程分爲4個步驟:

(1)初始標記(CMS initial mark),須要STW,只是標記GC Roots能直接關聯到的對象,速度很快;

(2)併發標記(CMS concurrent mark),進行GC Roots Tracing的過程;

(3)從新標記(CMS remark),須要STW,爲了修正併發標記階段因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段停頓時間比初始標記階段稍長一些,可是遠比並發標記的時間短;

(4)併發清除(CMS concurrent sweep)

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

CMS是一款優秀的收集器,它的主要優勢:併發收集、低停頓。

可是也有以下3個明顯的缺點:

(1)CMS收集器對CPU資源很是敏感:其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程而致使應用程序變慢,總吞吐量下降。CMS默認啓動的回收線程數是(sizeof(Cpu) + 3)/ 4。

(2)CMS收集器沒法處理浮動垃圾,可能出現Concurrent Mode Failure失敗而致使另外一次Full GC的產生。因爲CMS並清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法再當次收集中處理掉它們,只好留待下一次GC時再清理掉。這部分垃圾就稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,那也就須要預留有足夠的內存空間給用戶線程使用,所以CMS不能像其餘收集器那樣等到老年代幾乎徹底填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用,CMS在老年代佔用到92%時運行CMS,能夠經過-XX:CMSInitiatingCccupancyFraction的值來改變。若是CMS運行期間預留的空間沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時虛擬機將啓動後備預案:臨時啓動Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。

(3)CMS是一款基於「標記 - 清除」算法實現的收集器(由於併發清除階段,用戶線程不停頓,無法使用標記 - 整理算法)。意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCOmpactAtFullCollection開關參數(默認是開啓的),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,標示每次進入Full  GC時都進行碎片整理)

4.7 G1

Garbage-First:一款面向服務器端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)將來能夠替換掉CMS收集器。

G1收集器的運行大體可劃分爲如下幾個步驟:

(1)初始標記(Initial marking):標記如下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短。

(2)併發標記(Concurrent marking):從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這階段耗時較長,但可與用戶程序併發執行。

(3)最終標記(Final marking):爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,須要停頓線程,可是可並行執行。

(4)篩選回收(Live data couting and evacuation):首先對各個Region的回收價值和成本記性排序,根據用戶所指望的GC停頓時間來指定回收計劃。須要停頓,只回收一部分Region,時間上是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。

與其餘收集器相比,G1具有以下特色:

(1)並行與併發:能充分利用多CPU,多核環境下的硬件優點。

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

(3)空間整合:G1從總體上看是基於「標記 - 整理」算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法。都不會產生內存空間碎片。

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

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

G1之因此可以創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所須要時間的經驗值),在後天惟一一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(G1名稱的由來)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。

5. GC日誌

每一種收集器的日誌形式都是由它們自身的實現所決定的,即每一個收集器的日誌格式均可以不同,可是各個收集器的日誌也有必定的共性。

 

 

6. 配置垃圾收集器及參數

6.1 配置垃圾收集器

從上面圖中能夠看出新生代與老年代之間的收集器一共有6種組合,下面經過實例驗證收集器:

6.1.1 UseSerialGC(client模式下的默認值)

打開此開關後,使用Serial + Serial Old的收集器組合進行內存回收。

如何設置Serial + CMS + Serial Old ?

實例:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[6 * _1MB];
byte[] b2 = new byte[4 * _1MB]; } } 

設置jvm的參數爲:

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

-Xms20M -Xmx20M參數限制Java堆大小爲20MB,不可擴展。

-Xmn10M其中10MB分配給新生代,剩下的10MB分配給老年代。

-XX:SurvivorRatio=8設置了新生代中Eden區與一個Survivor區的空間比例爲8:1,即Eden爲8MB,to Survivor與from Survivor爲1MB,即新生代可用空間爲9MB(Eden區加一個Survivor區)。

-XX:+PrintGCDetails打印GC詳細信息。

-XX:+UseSerialGC使用Serial + Serial Old的收集器組合進行內存回收

 運行上面的程序,控制檯打印以下的GC日誌和堆信息:

過程以下:

6.1.2 UseParNewGC【deprecated】

使用ParNew + Serial Old的收集器組合。

實例:

將上面實例中的收集器修改成-XX:+UseParNewGC。

GC日誌與使用Serial + Serial Old的收集器相似,只是有個提示:未來會移除這種收集器的組合,主要緣由是這種組合方式的收集器不多使用,可是卻花費了很大的開發,維護和測試。具體能夠參考:【http://openjdk.java.net/jeps/173】,【http://openjdk.java.net/jeps/214】

6.1.3 UseConcMarkSweepGC

使用ParNew + CMS + Serial Old收集器組合,其中Serial Old收集器做爲CMS出現Concurrent Mode Failure失敗後的後備收集器。

將上面實例中的收集器修改成-XX:+UseConcMarkSweepGC。

GC日誌也是相似的,能夠看出不一樣的收集器對新生代和老年代的命名是有部分區別的

6.1.4 UseParallelGC(server模式下的默認值)

使用Parallel Scavenge + Serial Old(PS MarkSweep)

將上面實例中的收集器修改成-XX:+UseParallelGC。

奇怪,Eden不足,卻沒有發生GC,直接將4MB存入了老年代。

修改代碼:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[6 * _1MB];
        byte[] b2 = new byte[3 * _1MB];
    }

}

經過對比發現,當整個新生代剩餘的空間(Eden加一個Survivor)沒法存放某個對象時,Parallel Scavenge/Parallel Old中該對象會直接進入老年代;

而若是整個新生代剩餘的空間能夠存放但只是Eden區空間不足,則會嘗試一次Minor GC;

而對於Serial/Serial Old當發現Eden區不足以存放對象時,就進行一次Minor GC。

此外,爲何觸發了一次新生代GC,而後又觸發了一次Full GC呢?

其實,Parallel Scavenge(-XX:+UseParallelGC)框架下,默認是在要觸發full GC前先執行一次young GC,而且兩次GC之間能讓應用程序稍微運行一小下,以期下降full GC的暫停時間(由於young GC會盡可能清理了young gen的死對象,減小了full GC的工做量)

6.1.5 UseParallelOldGC

使用Parallel Scavenge + Parallel Old

修改使用-XX:+UseParallelOldGC

也是沒有觸發GC的,第二次分配改爲3MB就會觸發GC,原理同(4)UseParallelGC。

 

6.2 參數

(1)SurvivorRatio: 新生代中Eden區域與Survivor區域的容量比值,默認爲8,表明Eden:Survivor = 8:1

(2)PretenureSizeThreshold: 直接晉升到老年代的對象大小,設置這個參數後,大於這個參數時對象將直接在老年代分配。該參數只對新生代的Serial和ParNew收集器才起做用。實例見下面章節【大對象直接進入老年代】

(3)MaxTenuringThreshold: 晉升到老年代的對象年齡,每一個對象在堅持過一次Minor GC以後,年齡就增長1,當超過這個參數值時進入老年代

(4)UseAdaptiveSizePoliy: 動態調整Java堆中個區域的大小以及進入老年代的年齡

(5)HandlePromotionFailure: 是否容許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的全部對象都存活的極端狀況

(6)ParallelGCThreads: 設置並行GC時進行內存回收的線程數

(7)GCTimeRatio: GC時間佔總時間的比率,默認值爲99,即容許1%的GC時間。僅在使用Parallel Scavenge收集器時生效

(8)MaxGCPauseMillis: 設置GC的最大停頓時間,僅在使用Parallel Scavenge收集器時生效。

(9)CMSInitiatingOccupancyFraction: 設置CMS收集器在老年代空間被使用多少後觸發垃圾收集。默認值爲68%,僅在使用CMS收集器時生效

(10)UseCMSCompactAdFullCollection: 設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理,僅在使用CMS收集器時生效

(11)CMSFullCsBeforeCompaction: 設置CMS收集器在進行若干次垃圾收集器再啓動一次內存碎片整理,僅在使用CMS收集器時生效 

7. 內存分配與回收策略

對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按照線程優先在TLAB上分配,少數狀況下也可能會直接分配在老年代中。分配規則並非固定的,其細節取決於當前使用的哪種垃圾收集器組合,還有虛擬機中與內存相關的參數設置。

7.1 對象優先在Eden分配

7.2 大對象直接進入老年代

大對象:須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串和數組。

虛擬機提供了一個-XX:PretenureSizeThread參數,令大於這個設置值的對象直接在老年代分配,這樣能夠避免在Eden區和兩個Survivor區直接發生大量的內存複製。

測試代碼:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[4 * _1MB];
    }

}

VM參數

-server -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
-XX:+UseSerialGC -XX:PretenureSizeThreshold=5242880

即大於等於5MB的對象纔會直接分配到老年代中,分別測試分配4MB和6MB的對象,而後堆的狀況:

4MB的堆:

6MB的堆:

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

虛擬機給每一個對象定義了一個對象年齡計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設置爲1,。對象在Survivor中每熬過一次Minor GC,年齡就增長1,當它的年齡增長到必定程度(默認15),就會晉升到老年代中。

對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置。

7.4 動態對象年齡斷定

虛擬機並非永遠要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

7.5 空間分配擔保

  1. 在發生Minor以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間;
  2. 若是這個條件成立,那麼Minor GC能夠確保是安全的,則執行Minor GC;
  3. 若是不成立,虛擬機會接着查看HandlePromotionFailure設置值是否容許擔保失敗;
  4. 若是容許,會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小;
  5. 若是大於,將嘗試一次Minor GC,若是小於或HandlePromotionFailure設置不容許,則進行一次Full GC

 

從JDK6 Update 24以後,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略。

從JDK 6 Update 24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC ,不然將進行Full GC。

  

x. 參考資料

http://blog.csdn.net/canot/article/details/51069424

http://blog.csdn.net/z69183787/article/details/51606410

http://openjdk.java.net/jeps/173

http://openjdk.java.net/jeps/214

相關文章
相關標籤/搜索