JVM詳解2.垃圾收集與內存分配


點擊進入個人博客

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的「高牆」,牆外的人想進來,牆裏面的人卻想出來。

2.1 對象是否須要回收

2.1.1 引用計數法算法

原理:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器減1,任什麼時候刻計數器都爲0的對象就是不可能再被使用的。
優勢:實現原理簡單,並且斷定效率很高。
缺點:很難解決對象之間相互循環引用的問題。html

2.1.2 可達性分析算法

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

Java中的GC Roots對象
  1. 虛擬機棧(棧楨中的本地變量表)中的引用的對象
  2. 本地方法棧中JNI(通常說的Native方法)的引用的對象
  3. 方法區中的類靜態屬性引用的對象
  4. 方法區中的常量引用的對象

2.1.3 什麼是引用

不管是經過引用計數算法判斷對象的引用數量,仍是經過根搜索算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。算法

JDK 1.2 以前

在JDK1.2以前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用
缺點:一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,咱們但願能描述這樣一類對象——當內存空間還足夠時,則能保留在內存之中;若是內存在GC以後仍是很是緊張,則能夠拋棄這些對象(如緩存)。數組

JDK 1.2 以後

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

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

更多資料:深刻探討 java.lang.ref 包慎用java.lang.ref.SoftReference實現緩存安全

2.1.4 finalize()

兩次標記過程

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

  1. 對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選。篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」,對象被回收。
  2. 若是這個對象有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣作的緣由是,若是一個對象finalize()方法中執行緩慢,或者發生死循環,將極可能會致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至致使整個內存回收系統崩潰。
  3. Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記。若是對象要在Finalize()中成功拯救本身——只要從新與引用鏈上的任何的一個對象創建關聯便可,那在第二次標記時它將移除出「即將回收」的集合。若是對象這時候還沒逃脫,那基本上它就真的被回收了。
使用finalize()自我救贖
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I am still alive");
    }

    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        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 QAQ!");
        }
 
        // 以上代碼與上面的徹底相同,但此次自救卻失敗了!!!
        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 QAQ!");
        }
    }
}
總結
  • System.gc()底層調用的是Runtime.getRuntime().gc();,該方法的Java doc裏邊寫的是調用此方法suggestsJVM進行GC,即沒法保證對垃圾收集器的調用。
  • finalize()方法至多由GC執行一次,用戶固然能夠手動調用對象的finalize方法,但並不影響GC對finalize()的行爲。
  • 雖然能夠在finalize()方法完成不少操做如關閉外部資源,但更好的方式應該是try-finally
  • finalize()運行代價高昂,不肯定大,沒法保證各個對象的調用順序。
  • 最好的方法就是忘掉有這個方法!

2.1.5 回收方法區

Java虛擬機規範不要求虛擬機在方法區實現垃圾收集;方法區的GC性價比通常比較低。
方法區的GC主要是回收兩部份內容:廢棄常量和無用的類。併發

廢棄常量

判斷常量是否廢棄跟對象是同樣。常量池中的其餘類、接口、方法、字段的符號引用也是如此。框架

無用的類(必須同時知足如下三個條件)
  1. 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
  2. 加載該類的ClassLoader已經被回收;
  3. 該類對應的Java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
類是否回收
  • 知足上述3個條件的類只是被斷定爲能夠被虛擬機回收,而不是和對象同樣,不使用了基於就必然會回收。是否對類進行回收,還須要對虛擬機進行相應的參數設置。
  • 在HotSpot中,虛擬機提供-Xnoclassgc參數進行控制,還可使用-verbose:class以及-XX:+TraceClassLoading-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class-XX:+TraceClassLoading能夠在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數須要FastDebug版的虛擬機支持。
  • 在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載功能,以保證永久代不會溢出。

2.2 垃圾收集算法

2.2.1 標記-清除算法

定義:標記-清除(Mark-Sweep)算法分爲標記和清除兩個階段,首先標記出須要回收的對象,標記完成以後統一清除對象。
缺點:效率問題,標記和清除過程效率不高;標記清除以後會產生大量不連續的內存碎片。ide

2.2.2 複製算法

定義:複製(Copying)算法它將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊用完以後,就將還存活的對象複製到另一塊上面,而後在把已使用過的內存空間一次理掉。
優勢:這樣使得每次都是對其中的一塊進行內存回收,不會產生碎片等狀況,只要移動堆訂的指針,按順序分配內存便可,實現簡單,運行高效。
缺點:內存縮小爲原來的一半。
使用狀況:如今的商業虛擬機都採用這種收集算法來回收新生代,新生代中的對象98%都是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。
HotSpot虛擬機:默認Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。

2.2.3 標記-整理算法

定義:標記-整理算法的標記過程與標記清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是對全部存活的對象都向一端移動,而後清理掉邊界之外的內存。
優勢:解決了複製算法在對象存活率較高狀況下須要大量複製致使的效率問題,並且不會縮小內存。

2.2.4 分代收集算法

定義:根據對象存活週期的不一樣將內存分爲幾塊,通常是把Java堆分爲新生代和老年代,根據各個年代的特色採用最適用的算法。
新生代:每次收集都會有大批對象死去,只有少許存活,採用複製算法。
老年代:對象存活率較高、沒有額外空間對它進行分配擔保,採用標記-清除或標記-整理算法。

2.3 HotSpot算法實現

2.3.1 枚舉根節點

可達性分析的效率問題:可做爲GC Roots的節點主要在全局性的引用(常量或類的靜態屬性)與執行上下文(如棧幀的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是逐個檢查引用必然會消耗不少時間。
GC停頓:可達性分析在分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,這就是致使GC進行時必須停頓全部Java執行線程(Sun將這件事情成爲「Stop The World」)的一個重要緣由,即便在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉跟結點也是必需要暫停的。
準確是GC:主流JVM都使用的是準確式GC,即JVM知道內存中某位置的數據類型什麼,因此當執行系統停下來的時候,不須要一個不漏的檢查完全部執行上下文和全局的引用位置,虛擬機能夠有辦法知道哪些地方存放着對象的引用。
HotSpot的OOPMap:在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來;在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣GC在掃描的時候就能夠直接得到這些信息。

2.3.2 安全點

爲何須要安全點:有了OOPMap,HotSpot能夠快而準的完成GC Roots的查找,但若是爲每一行代碼的指令都生成OOPMap,這樣將佔用大量的空間。因此HotSpot並無這麼作!
安全點:HotSpot只在特定的位置記錄了OOPMap,這些位置稱爲安全點(Safe Point),即程序不能在任意地方均可以停下來進行GC,只有到達安全點時才能暫停進行GC。

安全點的選擇

安全點的選定基本上是以「是否具備讓程序長時間執行的特徵」進行選定的,既不能選擇太少以至於讓GC等待過久,與不能太頻繁以至於增大系統負荷。具體的安全點有

  1. 循環的末尾
  2. 方法返回前
  3. 調用方法的call以後
  4. 拋出異常的位置
GC時讓全部線程停下來
  • 搶先式中斷:不須要線程的執行代碼主動配合,在GC時先把全部線程中斷,而後若是有線程沒有運行到安全點,則恢復線程讓他們運行到安全點。幾乎沒有JVM採用這種方式
  • 主動式中斷:當GC須要中斷線程的時候,不直接對線程操做而是設置一個標誌,各個線程執行時主動輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。

2.3.3 安全區域

安全點的不足:安全點機制保證了程序執行時,在較短的時間就會遇到能夠進入GC的安全點,但若是程序處於不執行狀態(如Sleep狀態或者Blocked狀態),這時候線程沒法相應JVM的中斷請求,沒法運行到安全點去中斷掛起,JVM也不會等待線程從新被分配CPU時間。
安全區域:安全區域(Safe Region)是指在一段代碼片斷之中,引用關係不會發生變化,這個區域的任何地方GC都是安全的。能夠把安全區域當作是擴展了的安全點。

安全區域工做原理
  1. 在線程執行到安全區域中的代碼時,首先標識本身已經進入了安全區域,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲安全區域狀態的線程了。
  2. 在線程要離開安全區域時,它要檢查系統是否已經完成了根節點枚舉,若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開安全區域的信號爲止。

3.4 垃圾收集器

這裏討論的收集器基於JDK 7 Update14的HotSpot虛擬機,這個版本中正式提供了商用的G1收集器。下圖展現了HotSpot虛擬機的垃圾收集器,若是兩個收集器存在連線,說明能夠搭配使用。
HotSpot虛擬機的垃圾收集器

3.4.1 Serial

簡介:最基本、最悠久、單線程
缺點:只會使用一條線程完成GC工做,並且在工做時必須暫停其餘全部工做線程。
優勢:簡單而高效(與其餘收集器的單線程比),是JVM運行在Client模式下的默認新生代收集器。
Serial/Serial Old收集器運行示意圖

使用方式

-XX:+UseSerialGC,設置以後默認使用Serial(年輕代)+Serial Old(老年代) 組合進行GC。

3.4.2 ParNew

簡介:Serial的多線程版本,其他行爲包括Serial的全部控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial徹底同樣,默認開啓的收集線程數與CPU數量相同。
優勢:多線程收集、能與CMS配合工做(這也是它是許多Server模式下虛擬機中首選的緣由)
缺點:單線程效率不及Serial。
ParNew/Serial Old收集器運行示意圖

使用方式
  1. 設置-XX:+UseConcMarkSweepGC的默認收集器
  2. 設置-XX:+UseConcMarkSweepGC強制指定
  3. 設置-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

3.4.3 Parallel Scavenge

簡介:新生代收集器、採用複製算法、並行多線程收集器、關注的目標是達到一個可控制的吞吐量而非儘量的縮短GC時用戶線程的停頓時間。
吞吐量:CPU用於運行用戶代碼的時間和CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。停頓時間越短適合與用戶交互的程序,良好的相應速度能提高用戶體驗;而高吞吐量能夠高效利用CPU時間,適合後臺運算。

使用方式
  1. -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,是一個大於0的毫秒數
  2. -XX:GCTimeRatio:直接設置吞吐量大小,是一個大於0且小於100的整數,默認值是99,就是容許最大1%即(1/(1+99))的垃圾收集時間。
  3. -XX:+UseAdaptiveSizePolicy:若是設置此參數,就不須要手工設定新生代的大小、Eden於Survivor區的比例、晉升老年代對象年齡等細節參數來,虛擬機會動態調整。

3.4.4 Serial Old收集器

簡介:Serial的老年代版本、單線程、使用標記整理算法
用途:主要是爲Client模式下的虛擬機使用;在Server模式下有兩大用途,一是在JDK 5及以前版本中配合Parallel Scavenge收集器一塊兒使用,而是做爲CMS的後備預案,在併發收集發生Concurrent Mode Failure時使用。

3.4.5 Parallel Old收集器

簡介:Parallel Scavenge的老年代版本、多線程、標記整理算法、JDK 6中才出現
用途:直到Parallel Old收集器出現後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,可使用Parallel Scavenge和Parallel Old的組合。
Parallel Scavenge和Parallel Old的組合

3.4.6 CMS

簡介:CMS(Concurrent Mark Sweep)以最短回收停頓時間爲目標、適合B/S系統的服務端、基於標記清除算法
優勢:併發收集、低停頓

工做流程
  1. 初始標記——須要Stop The World,僅僅標記一下GC Roots能直接關聯對象,速度很快
  2. 併發標記——進行GC Roots Tracing
  3. 從新標記——須要Stop The World,爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,速度很快
  4. 併發清除

CMS收集器

缺點
  1. 對CPU資源很是敏感,在併發階段它雖然不會致使用戶線程停頓,可是會由於佔用一部分線程(CPU資源)致使程序變慢
  2. CMS沒法處理「浮動垃圾」——浮動垃圾是在併發清理階段用戶線程產生的新的垃圾,因此可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。
  3. 因爲CMS在垃圾收集階段用戶線程還須要執行,因此不能像其餘收集器那樣等老年代幾乎填滿了再進行收集,因此須要預留一部分空間給用戶線程。CMS運行期間若是預留的內存沒法知足程序須要,就會出現「Concurrent Mode Failure」失敗,此時虛擬機將會臨時啓用Serial Old收集器來進行老年代的垃圾收集,致使長時間停頓。
  4. 因爲CMS基於標記清除算法,因此會致使內存碎片。

3.4.7 G1收集器

原理
  1. 堆內存劃分:G1收集器將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。
  2. 收集策略:G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回價值最大的Region(這也就是Garbage-First名稱的來由),有計劃地避免在整個Java堆中進行全區域的垃圾收集。
  3. Region不多是孤立的:把Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?仔細想一想就很容易發現問題所在:Region不多是孤立的。一個對象分配在某個Region中,它並不是只能被本Region中的其餘對象引用,而是能夠與整個Java堆任意的對象發生引用關係。那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保障準確性?這個問題其實並不是在G1中才有,只是在G1中更加突出了而已。在之前的分代收集中,新生代的規模通常都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象也面臨過相同的問題,若是回收新生代時也不得不一樣時掃描老年代的話,Minor GC的效率可能降低很多。
  4. 使用Remembered Set來避免全堆掃描:在G1收集器中Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查引是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。
優勢
  1. 並行與併發:G1能充分使用多CPU、多核來縮短Stop The World的停頓,部分其餘收集器須要停頓Java線程執行的GC動做,G1仍然能夠經過併發的方式讓Java線程繼續運行。
  2. 分代收集:保留了分代收集的概念,並且不須要其餘收集器配合能獨立管理整個堆。
  3. 空間整合:G1從總體看來是基於「標記-整理」算法實現的,從局部(兩個Region之間)是基於複製算法實現的,不會產生空間碎片。
  4. 可預測的停頓:G1能讓使用者明確制定在長度爲M毫秒內,消耗在GC上的時間不得超過N毫秒,這幾乎是實時Java(RTJS)的垃圾收集器的特徵了。
運做流程

G1運行流程

若是不計算維護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停頓時間來制定回收計劃,這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。

3.4.8 GC參數總結

參數 描述
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 的收集器組合進行內存回收
SurvivorRatio 新生代中 Eden 區域與 Survivor 區域的容量比值,默認爲8,表明 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配
MaxTenuringThreshold 晉升到老年代的對象年齡,每一個對象在堅持過一次 Minor 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 收集器在完成垃圾收集後是否要進行一次內存碎片整理,僅在使用 CMS 收集器時生效
CMSFullGCsBeforeCompaction 設置 CMS 收集器在進行若干次垃圾收集後再啓動一次內存碎片整理,僅在使用 CMS 收集器時生效

3.5 理解GC日誌

每一種收集器的日誌形式都是由它們自身的實現所決定的,換言之每一個收集器的日誌格式均可以不同。但虛擬機設計者爲了方便用戶閱讀,將各個收集器的日誌都維持必定的共性,例如如下兩段典型的GC日誌:

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]

100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
  1. 前面的數字(33.12五、100.667):表明GC發生的時間,即從JVM啓動以來通過的秒數
  2. [GC或[FullGC:表明此次GC的停頓類型,若是有「Full」說明此次GC是發生了Stop-The-World的。新生代也會出現「[Full GC」,這通常是由於出現了分配擔保失敗之類的問題,因此才致使STW)。
  3. [GC (System.gc())或[Full GC (System.gc()):說明是調用System.gc()方法所觸發的收集。
  4. [DefNew、[Tenured、[Perm等:表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集是密切相關的——上面樣例所使用的Serial收集器中的新生代名爲「Default New Generation」,因此顯示的是「[DefNew」;若是是ParNew收集器,新生代名稱就會變爲「[ParNew」,意爲「Parallel New Generation」;若是採用Parallel Scavenge收集器,那它配套的新生代稱爲「PSYoungGen」;老年代和永久代同理,名稱也是由收集器決定的。
  5. 內部方括號中的3324K->152K(11904K):GC前該內存區域已使用容量 -> GC後該內存區域已使用容量(該內存區域總容量)。
  6. 外部方括號中的3324K->152K(11904K):表示GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)。
  7. 0.0025925secs:該內存區域GC所佔用的時間,單位是秒。
  8. [Times:user=0.01 sys=0.00,real=0.02 secs]:user、sys和real與Linux的time命令所輸出的時間含義一致,分別表明用戶態消耗的CPU時間、內核態消耗的CPU事件和操做從開始到結束所通過的牆鍾時間(Wall Clock Time)。CPU時間與牆鍾時間的區別是,牆鍾時間包括各類非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操做會疊加這些CPU時間,因此讀者看到user或sys時間超過real時間是徹底正常的。詳細參見:Linux用戶態程序計時方式詳解

3.6 內存分配與回收策略

對象的內存分配總的來講,就是在堆上分配(但也可能通過JIT編譯後被拆散爲標量類型並間接地棧上分配);對象主要分配在新生代的Eden區上;若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配;少數狀況下也可能會直接分配在老年代中。分配的規則並非百分之百固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

3.6.1 Minor和Full GC

  • 新生代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倍以上。

3.6.2 對象優先在Eden分配

大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8
 */
public class Allocation {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        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 (Allocation Failure) [DefNew: 7482K->380K(9216K), 0.0061982 secs] 7482K->6524K(19456K), 0.0062260 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
Heap
  def new generation   total 9216K, used 4641K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
   eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)
   from space 1024K,  37% used [0x00000007bf500000, 0x00000007bf55f318, 0x00000007bf600000)
   to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
  Metaspace       used 2968K, capacity 4496K, committed 4864K, reserved 1056768K
   class space    used 327K, capacity 388K, committed 512K, reserved 1048576K
  1. -Xms20M、-Xmx20M、-Xmn10M、-XX:SurvivorRatio=8四個參數保證了整個Java堆大小爲20M,新生代10M(eden space 8192K、from space 1024K、to space 1024K)、老年代10M。
  2. 在給allocation4分配空間的時候會發生一次Minor GC,此次GC發生的緣由是給allocation4分配所需的4MB內存時,發現Eden區已經被佔用了6MB,剩餘空間不足以分配 4MB,所以發生Minor GC。
  3. [GC (Allocation Failure) :表示由於向Eden給新對象申請空間,可是Eden剩餘的合適空間不夠所需的大小致使的Minor GC。
  4. GC期間虛擬機又發現已有的3個2MB對象沒法所有放入Survivor空間(Survivor只有1MB),因此只好經過分配擔保機制提早轉移到老年代。
  5. 此次GC結束後,4MB的allocation4對象被順利分配到Eden中。所以程序執行完的結果是Eden佔用4MB(被allocation4佔用),Survivor空閒,老年代被佔用6MB(allocation1,2,3佔用)。

3.6.3 大對象直接進入老年代

什麼是大對象:大對象就是指須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組(byte[]數組就是典型的大對象)。
大對象的影響:大對象對虛擬機的內存分配來講就是一個壞消息(更加壞的狀況就是遇到一羣朝生夕死的短命 對象,寫程序時應該避免),常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置大對象。
設置大對象的參數:能夠經過-XX:PretenureSizeThreshold參數設置使得大於這個設置值的對象直接在老年代分配,避免在Eden區及兩個Survivor區之間發生大量的內存拷貝。

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728(3M)
 */
public class PretenureSizeThreshold {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation = new byte[4 * _1MB];
    }
}
Heap
 def new generation   total 9216K, used 1502K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  18% used [0x00000007bec00000, 0x00000007bed778d8, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
 Metaspace       used 2931K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 321K, capacity 388K, committed 512K, reserved 1048576K
  1. 咱們能夠看到Eden空間幾乎沒有被利用,而老年代10MB空間被使用40%,也就是4MB的allocation對象被直接分配到老年代中,這是由於PretenureSizeThreshold被設置爲3MB,所以超過3MB的對象都會直接在老年代中進行分配。
  2. PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器通常並不須要設置。若是遇到必須使用此參數的場合,能夠考慮ParNew加CMS的收集器組合。

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

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

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
 */
public class MaxTenuringThreshold {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB]; // Eden空間不足GC,allocation1進入Survivor
        allocation3 = null;
        allocation3 = new byte[4 * _1MB]; // Eden空間不足第二次GC
    }
}
[GC (Allocation Failure) [DefNew: 5690K->624K(9216K), 0.0052742 secs] 5690K->4720K(19456K), 0.0053049 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 4720K->0K(9216K), 0.0009947 secs] 8816K->4709K(19456K), 0.0010106 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4260K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4709K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  45% used [0x00000007bf600000, 0x00000007bfa99570, 0x00000007bfa99600, 0x00000007c0000000)
 Metaspace       used 2953K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 327K, capacity 388K, committed 512K, reserved 1048576K

此方法中allocation1對象須要256KB的內存空間,Survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後會很是乾淨地變成0KB。而 MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代Survivor空間,這時候新生代仍然有410KB的空間被佔用。

3.6.5 動態對象年齡斷定

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

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
 */
public class Main {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB]; // 第一次GC
        allocation4 = null;
        allocation4 = new byte[4 * _1MB]; // 第二次GC
    }
}
[GC (Allocation Failure) [DefNew: 5946K->880K(9216K), 0.0045988 secs] 5946K->4976K(19456K), 0.0046307 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 5058K->0K(9216K), 0.0012867 secs] 9154K->4965K(19456K), 0.0013125 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4315K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf036ce8, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf4000e0, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4965K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  48% used [0x00000007bf600000, 0x00000007bfad9500, 0x00000007bfad9600, 0x00000007c0000000)
 Metaspace       used 2957K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 327K, capacity 388K, committed 512K, reserved 1048576K

發現運行結果中Survivor佔用仍然爲0%,而老年代比預期增長了,也就是說allocation1,allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。由於這兩個對象加起來達到了512KB,而且它們是同年的,知足同年對象達到Survivor空間的一半規則。 咱們只要註釋一個對象的new操做,就會發現另一個不會晉升到老年代了。

3.6.5 空間分配擔保

  • Minor GC流程:在發生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。雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。
JDK 6 Update 24以後,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。
相關文章
相關標籤/搜索