JVM(2)垃圾收集器
1、對象存活
內存回收與分配重點關注的是堆內存和方法區內存(程序計數器占用小,虛擬機棧和本地方法棧隨線程有相同的生命周期)。
1.1、引用計數算法
給對象中添加一個引用計數,每當有一個地方引用它時,計數器加一;當引用失效時,計數器減一;當計數器值為0時,表示對象不會再被使用。
優點:實現簡單,判定效率高;
缺點:很難解決對象之間互相循環引用而無法回收的問題;
1.2、可達性分析算法
算法的基本思路是通過一些列的稱之為“GC Roots”的對象作為起始點,從這些節點向下搜索成為引用鏈,當一個對象到GC Roots沒有任何引用鏈,則判定死亡。Java語言中可作為GC Roots的對象包括以下幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中靜態屬性引用的對象(JDK1.8Metaspace取代)
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
1.3、對象引用類型
java中將引用分為四種類型:
- 強引用(Strong Reference):類似Object obj = new Object(),只要引用還在,對象就不會被jvm回收;
- 軟引用(Soft Reference):還有用但并非必需的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
- 弱引用(Weak Reference):弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當如前內存是否足夠,都會回收掉只被弱引用關聯的對象。
- 虛引用(Phantom Reference):虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例.為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一了個系統通知。
1.4、對象的標記回收過程
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宜告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize()方法。當對象沒有覆蓋 finalize() 方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行” 。
如果這個對象被判定為有必要執行 finalize()方法,那么這個對象將會放置在一個叫做 F-Queue 的隊列之中, ,并在稍后由一個由虛擬機自動建立的、低優先級的 Finalizer 線程去執行它.這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個對象在 finalizer 方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致 F-Queue 隊列中其他對象永久處于等待,甚至導致整個內存回收系統崩潰. finalize()方法是對象逃脫死亡命運的最后一次機會,稍后 GC 將對 F-Queue 中的對象進行第二次小規模的標記,如果對象要在 finalize ()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己( this 關鍵字)賦值給某個類變量或者對象的成員變斌,那在第二次標記時它將被移除出“即將回收”的集合:如果對象這時候還沒有逃脫,那基本上它就真的被回收了。
1.5、回收方法區
永久代的垃圾收集主要回收兩部分內容;廢棄常量和無用的類。回收廢棄常量與回收 Java 堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字符串“ abe ”已經進人了常量池中,但是當前系統沒有任何一個 string 對象是叫做“ abc ”的,換句話說,就是沒有任何 string 對象引用常量池中的“ abc ”常量,也沒有其他地方引用了這個字面最,如果這時發生內存回收,而且必要的話,這個“ abc ”常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多.
類需要同時滿足下面 3 個條件才能算是“無用的類” :
- 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例;
- 加載該類的 ClassLoader 已經被回收;
- 該類對應的 java.lang.class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
2、垃圾收集算法
2.1、標記-清除算法:
最基礎的收集算法是“標記一清除” ( Mark 一 Sweep )算法,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
標記-清除.png
主要有兩個不足:
- 效率問題,標記和清除兩個過程的效率都不高;
- 空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
2.2、復制算法
復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。
2.3、標記-整理算法
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費 50 %的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端清況,所以在老年代一般不能直接選用這種算法。報據老年代的特點,提出了另外一種“標記一整理” ( Mark-Compact 的算法,標記過程仍然與“標記一清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
標記整理.png2.4、分代收集思想
分代收集算法是根據對象存活周期的不同將內存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少錄存活,就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記一清理”或者“標記一整理”算法來進行回收。
3、HotSpot算法實現
3.1、枚舉根節點
GC Root查找引用鏈的缺點:
- 很多應用僅僅方法區就有數百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間。
- 在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關系還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致 GC 進行時必須停頓所有 Java 執行線程的其中了個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS 收集器中仁枚舉根節點時也是必須要停頓的。
在 HotsPoL 的實現中,是使用一組稱為OopMap 的數據結構來存放對象的引用的,在類加載完成的時候, HotSpot就把對象什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用 · 這樣, GC 在掃描時就可以直接得知引用信息。
3.2、安全點
Hotspot能通過OopMap快速完成GC Root的遍歷,但能引起OopMap內容變化的之類非常多,若為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣 GC的空間成本將會變得很高。
實際上, Hotspot只是在“特定的位置”記錄OopMap信息,這些位置稱為安全點( safepoino ,即程序執行時并非在所有地方都能停頓下來開始 GC ,只有在到達安全點時才能暫停。 一般選擇方法調用、循環跳轉、異常跳轉等指令作為safepoint。
對于safepoint還需要考慮的問題是,如何在GC發生時,讓所有線程都在safepoint停下來。
方案有如下兩種:
搶先式中斷:搶先式中斷不需要線程的執行代碼主動去配合,在 GC 發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓其運行到安全點上中斷。現在幾乎沒有虛擬機實現采用搶先式中斷來暫停線程從而響應 GC 事件。
主動式中斷:當 GC 需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。
行 JNI 調用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:搶先式中斷( Preemptivc suspension )和主動式中斷( volunta 口 suspension ) ,其‘ } ,搶先式中斷不需要線程的執行代碼主動去配合,在 GC 發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機實現采用搶先式中斷來暫停線程從而響應 GC 事件。而主動式中斷的思想是當 GC 需要中斷線程的時候,不直接對線程操作,僅僅簡單地設攤一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。下面代碼清單 3 礴中的 test 指令是 Hotspot 生成的輪詢指令,當需要暫停線程時,虛擬機把。、 1 60 100 的內存頁設置為不可讀,線程執行到 test 指令時就會產生一個自陷異常信號,在預先注冊的異常處理器中暫停線程實現等待,這樣一條匯編指令便完成安全點輪詢和觸發線程中斷。
safepoint 的選定既不能太少以致于讓 GC 等待時間太長,也不能過于頻繁以致于過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的 ― 因為每條指令執行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執行”的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令才會產生 safepoint 。
3.3、安全區域
使用 safepoint 似乎已經完美地解決了如何進人 Gc 的問題,但實際情況卻并不一定。 safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進人 GC 的 safepoint 。但是,當程序未運行時,即沒有分配 CPU 時間時,典型的例子就是線程處于 sleep 狀態或者 Blocked 狀態,這時候線程無法響應 JVM 的中斷請求,運行到安全的地方去中斷掛起, JVM 也顯然不太可能等待線程重新被分配 CPU 時間。對于這種情況,就需要安全區域( safe Region )來解決.安全區域是指在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始 GC 都是安全的,我們也可以把 Safe Region 看做是被擴展了的 safepoint 。
在線程執行到 SafeRegion中的代碼時,首先標識自己已經進人了 Safe Region狀態, 這段時間里當JVM要發起 GC 時,就不用管標識自己為 safe Region 狀態的線程了。在線程要離開safe region區域時,它要檢查系統是否已經完成了根節點枚舉(或者是整個 GC 過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開 safe Region 的信號為止。
4、垃圾收集相關概念
4.1、垃圾收集性能指標
- 吞吐量:應用程序運行時間 /(應用程序運行時間+垃圾收集時間),即沒有花在垃圾收集的時間占總時間的比例。
- 垃圾收集開銷:與吞吐量相對,這表示垃圾收集耗用時間占總時間的比例。
- 暫停時間:在垃圾收集操作發生時,應用程序被停止執行的時間。
- 收集頻率:相時于應用程序的執行,垃圾收集操作發生的頻率。
- 堆空間:堆空間所占內存大小。
- 及時性:一個對象由成為垃圾到被回收所經歷的時間。
4.2、分代收集模型
分代收集( Generation Collection)是指在內存空間中劃分不同的區域,在各自區域中分別儲存不同年齡的對象,每個區域可根據自身特點靈活采用適合自身的收集策略。
根據存儲對象年齡的不同,可將分代分為 3 種類型:
- 新生代( Young Generation ) 分Eden去和2個Survivor區;
- 老年代( old Generation ) ;
- 永久代( Permancnt Gencra 石 on ) ;
根據垃圾收集作用在不同的分代,垃圾收集類型’分為兩種:
- Minor Collection :時新生代進行收集;
- Full Collection :除了時新生代收集外,也對老年代或永久代進行收集,又稱為 Major Collection。Full Collection 對所有分代都進行收集:首先,按照新生代配置的收集算法對新生代進行收集;接著,使用老年代收集算法對老年代和永久代進行收集.一般來說,相較于 Minor Collection ,這種收集行為的頻率較低,但耗時較長。
當分配空間時,首先在新時代的Eden區進行分配,當Eden區內存耗盡時,即觸發Minor GC;在復制環節,會將存活的對象復制到一個Survivor區,若Survivor區被占滿,將允許一部分對象晉升到老年代。當晉升時發現老年代無多余空間時,將通過Full GC對整個堆空間進行回收(CMS除外)。
JVM通過兩個參數判定某個對象是否可以晉升到老年代:
- 年齡:在 Minor GC 后仍然存活的時象,其經歷的 Minor GC 次數,就表示該對象的年齡。
- 大小:時象占用的空間大小。
4.3、快速分配
通常情況下,系統中有大最連續的內存塊可以用來分配對象,這種情況下若使用碰撞指針 ( bump-the-pointer )算法來進行對象內存空間分配的話,效率是很可觀的。這種算法的思路是:記錄下一次分配對象的位置,當有新的對象要分配時,若檢查剩余的空間能夠滿足容納這個對象,則只需要一次移動指針的操作便完成了內存的分配。
對于多線程應用,分配操作需要保證線程安全,如果通過全局鎖的方式來保證線程安全的內存分配將會成為性能瓶頸。所以 Hotspot 采用的是線程局部分配緩存技術(即 Thread-Local Allocation Buffers ,簡稱 TLABs )。每個線程都會有它自己的TLAB,位于 Eden 區中的一小塊空間。因為每個 TLAB 是僅對一個線程是可見的,所以分配操作可以使用 bump-the-pointer 技術快速完成,而不必使用任何鎖機制:只有當線程將TLAB 填滿并且需要獲取一個新的 TLAB 時,同步才是必須的。在虛擬機開啟了 UseTLAB 選項的前提下,在分配一個新的對象空間時,將首先嘗試在 TLAB 空間中分配對象空間,若分配空間請求失敗,則再嘗試使用加鎖機制在 Eden 中分配對象。
4.4、棧上分配和逸出分析
在棧上分配的基本思路是這樣的:分析局部變最的作用域僅限于方法內部.則 JVM 直接在棧幀內分配對象空間,避免在堆中分配。這個分析過程稱為逸出分析,而棧幀內分配對象的方式稱為棧上分配。這樣做的目的是減少新生代的收集次數,間接提高JVM 性能。
4.5、垃圾收集器的設計演進
- 根據不同分代的特點,收集器可能不同。有些收集器可以同時作用于新生代和老年代。而有些時候,我們則需要分別為新生代或老年代選用合適的收集器.一般來說,新生代收集器的收集須率較高,應當選用性能高效的收集器;而老年代收集器收集次數相對較少,但對空間較為敏感,應當避免選擇基于復制算法的收集器。
- 在垃圾收集執行的時刻,應用程序需要暫停運行;
- 可以串行收集,也可以并行收集;
- 如果能做到并發收集(應用程序不必暫停),那將是很妙的事情;
- 如果收集行為可控,那將是很妙的亨情。
- 收集器的類型決定了堆的類型。收集器掌控著諸如收集算法、時象分配策略、 STW 這些行為,因此需要‘意氣相投”的堆與之匹配,才能允許這些行為得以貫徹。比如,收集器基于“標記一清除”算法還是“標記一壓縮”算法,將影響堆內存的分配和回收方式。
5、垃圾收集器介紹
5.1、Serial收集器
Serial收集器是一個單線程的收集器,它的“單線程”的意義并不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
它也有著優于其他收集器的地方:簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用基本上不會再大了),停頓時間完全可以控制在幾十毫秒最多一百多毫秒以內,只要不是頻繁發生,這點停頓是可以接受的。所以,Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇。
Serial.png5.2、ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其與行為包括Serial收集器都可用的所有控制參數(例如 :-XX:SurvivorRatio、-XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure 等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在現實上,這兩種收集器也共用了相當多的代碼。
ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒有太多創新之處,但它卻是許多運行在Server,模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial 收集器外,目前只有它能與CMS收集器配合工作。
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。當然,隨著可以使用的CPU的數量的增加,它對于GC時系統資源的有效利用還是很有好處的。
ParNew.png5.3、Parallel Scavenge收集器
Parallel scavenge 收集器是一個新生代收集器,它是使用復制算法且是并行的多線程收集器。Parallel scavenge 收集器的特點是它的關注點與其他收集器不同,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自適應的調節策略。
5.4、Serial Old收集器
Serial old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用“標記一整理”算法。這個收集器的主要意義也是在于給 client 模式下的虛擬機使用。如果在 server 模式下,那么它主要還有兩大用途:一種用途是在 JDK 1.5 以及之前的版本中與 Parallel scavenge 收集器搭配使用。,另一種用途就是作為 CMS 收集器的后備預案,在并發收集發生 Concurrcnt Mode Failure 時使用。這兩點都將在后面的內容中詳細講解。
SerialOld.png5.5、Parallel Old收集器
Parallel old 是 Parallel scavenge 收集器的老年代版本,使用多線程和“標記一整理”算法。如果新生代選擇了 Parallel Scavenge 收集器,老年代除Serial Old ( Ps Markswccp )收集器外別無選擇,出于老年代 Serial Old 收集器在服務端應用性能上的“拖累”,使用了 Parallel Scavcnge 收集器也未必能在整體應用上獲得吞吐量最大化的效果,出于單線程的老年代收集中無法充分利用服務器多 CPU 的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有 ParNew 加 CMS 的組合“給力”。直到Parallel Old 收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的應用組合,在注重吞吐星以及 CPU 資源敏感的場合,都可以優先考慮 Parallcl Scavcnge 加 Parallel Old 收集器。
ParallelOld.png5.6、CMS收集器
CMS( Concurrent Mark sweep )收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分Java應用集中在互聯網站或者 B / S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
CMS收集器是基于“標記一清除”算法實現的,其整個過程分為四步:
- 初始標記(CMS initial mark):需要STW,僅標記GC Roots能直接關聯到的對象,速度很快。
- 并發標記(CMS concurrent mark):此階段就是進行GC Roots tracing的過程;
- 重新標記(CMS remark):此階段則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
- 并發清除(CMS concurrent sweep)
由于整個過程中耗時最長的并發標記和并發清除過程收集器線程都可以與用戶線程一起運作,所以,從總體上來說, CMS 收集器的內存回收過程是與用戶線程一起并發執行的。
cms.pngCMS的缺點為:
- CMS對CPU資源非常敏感:CMS在并發階段會占用一部分CPU資源,導致用戶程序變慢,總吞吐量降低。CMS默認回收線程數(CPU + 3)/ 4,也就是當 CPU 在 4 個以上時,并發回收時垃圾收集線程不少于25 %的 CPU 資源,且隨著 CPU 數量的增加而下降。但是當 CPU 不足 4 個(譬如 2 個)時, CMS 對用戶程序的影響就可能變得很大,如果本來 CPU 負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了 50 % ,其實也讓人無法接受。
- CMS收集器無法處理浮動垃圾:由于 CMS 并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后, CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此 CMS 收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程序運作使用。在 JDK1.5 的默認設置下, CMS 收集器當老年代使用了 68 % 的空間后就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調高參數 -xx : CMSlnitiatingOccupancyFraction 的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能,在 JDK 1.6 中, CMS 收集器的啟動閥值已經提升至 92 %。要是 CMS 運行期間預留的內存無法滿足程序需要,就會出現一次 " Concurrent Mode Failure ”失敗,這時虛擬機將啟動后備預案: 臨時啟動Serial old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數 -XX : CMSlnitiatingOccupancyFraction 設置得太高很容易導致大量“ Concurrent Mode Failure " 失敗,性能反而降低。
- 收集結束時會有大量空間碎片產生:CMS 是基于“標記一清除”算法實現的收集器,這意味著收集結束時會有大量空問碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次 Full GC 。為了解決這個問題, CMS 收集器提供了一個 -XX:+useCMScompact AtFullCollection 開關參數(默認就是開啟的),用于在 CMS 收集器頂不住要進行 Full GC 時開啟內存碎片的合并整理過程,內存整理的過程是無法并發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction ,這個參數是用于設置執行多少次不壓縮的 Full GC 后,跟著來一次帶壓縮的(默認值為 0 ,表示每次進人 Full GC 時都進行碎片整理)。
5.7、G1收集器
G1收集器有如下特點:
- 并行與并發: G1能充分利用多 CPU 、多核環境下的硬件優勢,使用多個 CPU ( CPU 或者 CPU 核心)來縮短STW停頓時間,部分其他收集器原本需要停頓 Java 線程執行的 GC 動作, G1 收集器仍然可以通過并發的方式讓 Java 程序繼續執行。
- 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個 GC 堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次 GC 的舊對象以獲取更好的收集效果。
- 空間整合:與 CMS 的“標記一清理”算法不同,G1從整體來看是從于“標記一整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基于“復制”算法實現的,但無論如何,這兩種算法都意味著 G1運作期間不會產生內存空問碎片,收集后能提供規整的可用內存。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次 GC 。
- 可預測的停頓:這是 G1相對于 CMS 的另一大優勢,降低停頓時間是 G1和 CMS 共同的關注點,但 G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集的時間不得超過 N 毫秒,這幾乎已經是實時 Java ( RTSJ )的垃圾收集器的特征了。
G1中堆內存布局:
G1將整個 Java 堆劃分為多個大小相等的獨立區域( Region ) ,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分 Region (不需要連續)的集合。
G1跟蹤各個 Region 里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先同收價值最大的 Region (這也就是 Garbage-First名稱的來由)。這種使用Region來劃分內存空間以及有優先級的區域回收方式,保證G1收集器在有限的時間內可以獲取盡可能高的收集效率。
在 Gl 收集器中, Region 之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set 來避免全堆掃描的。 G1中每個 Region 都有一個與之對應的 Remembered Set ,虛擬機發現程序在對 Reference 類型的數據進行寫操作時,會產生一個 Write Barrier暫時中斷寫操作,檢查 Reference 引用的對象是否處于不同的 Region 之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過Card Table 。把相關引用信息記錄到被引用對象所屬的 Region 的 Remembered Set 之中.當進行內存回收時,在 GC 根節點的枚舉范圍中加人 Remembered Set 即可保證不對全堆掃描也不會有遺漏。
如果不計算維護 Remembered set 的操作, GI 收集器的運作大致可劃分為以下幾個步驟:
- 初始標記( 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 停頓時間來制定回收計劃。
最后在篩選回收階段首先,從 Sun 公司透露出來的信息來看,這個階段其實也可以做到與用戶程序一起并發執行,但是因為只回收一部分 Region ,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。
5.8、垃圾收集器參數匯總
- 與串行回收器相關的參數
| -XX:+UseSerialGC | 在新生代和老年代使用串行收集器 |
| -XX:SurvivorRatio | 設置eden 區大小和survivor 區大小的比例 |
| -XX:PretenureSizeThreshold | 設置大對象直接進入老年代的閾值。當對象的大小超過這個值時,將直接在老年代分配。 |
| -XX:MaxTenuringThreshold | 設置對象進入老年代的年齡的最大值。每一次Minor GC 后,對象年齡就加1。任何大于這個年齡的對象, 一定會進入老年代 |
- 與并行GC相關的參數
| -XX:+UseParNewGC | 在新生代使用并行收集器 |
| -XX:+UseParallelOldGC | 老年代使用并行回收收集器 |
| -XX:ParallelGCThreads | 設置用于垃圾回收的線程數。通常情況下可以和CPU數量相等,但在CPU 數量較多的情況下,設置相對較小的數值也是合理的。 |
| -XX:MaxGCPauseMillis | 設置最大垃圾收集停頓時間。他的值是一個大于0 的整數。收集器在工作時,會調整Java 堆大小或者其他參數,盡可能把停頓時間控制在MaxGCPauseMillis 以內。 |
| -XX:GCTimeRatio | 設置吞吐量大小。它是0-100 的整數。假設GCTimeRatio 的值為n,那么系統將花費不超過1/(1+n) 的時間用于垃圾收集。 |
| -XX:+UseAdaptiveSizePolicy | 打開自適應GC 策略。在這種模式下,新生代的大小、eden 和survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。 |
- 與CMS 回收期相關的參數
| -XX:+UseConcMarkSweepGC | 新生代使用并行收集器, 老年代使用CMS+ 串行收集器 |
| -XX:ParallelCMSThreads | 設定CMS 的線程數量 |
| -XX:CMSInitiatingOccupancyFraction | 設置CMS 收集器在老年代空間被使用多少后觸發,默認為68% |
| -XX:+UseCMSCompactAtFullCollection | 設置CMS 收集器完成垃圾收集后是否要進行一次內存碎片的整理 |
| -XX:CMSFullGCsBeforeCompaction | 設定進行多少次CMS 垃圾回收后,進行一次內存壓縮 |
| -XX:+CMSClassUnloadingEnabled | 允許對類元數據區進行回收 |
| -XX:CMSInitiatingPermOccupancyFraction | 當永久區占用率達到這一百分比時,啟動CMS 回收(前提是-XX:+CMSClassUnloadingEnabled 激活了) |
| -XX:UseCMSInitiatingOccupancyOnlyn | 表示只在到達閾值的時候才進行CMS回收 |
| -XX:+CMSIncrementalMode | 使用增量模式, 比較適合單CPU。增量模式在JDK8 中標記為廢棄,并將在JDK 9 中徹底移除。 |
- 與G1回收器相關的參數
| -XX:+UseG1GC | 使用G1 回收器 |
| -XX:MaxGCPauseMillis | 設置最大垃圾收集停頓時間 |
| -XX:GCPauseIntervalMillis | 設置停頓間隔時間 |
- 與TLAB相關的參數
| -XX:+UseTLAB | 開啟TLAB 分配 |
| -XX:+PrintTLAB | 打印TLAB 相關分配信息 |
| -XX:TLABSize | 設置TLAB 大小 |
| 自動調整TLAB 大小 |
- 其他參數
| -XX:+DisableExplicitGC | 禁用顯式GC |
| -XX:+ExplicitGCInvokesConcurrent | 使用并發方式處理顯式GC |
5.9、垃圾收集器對比
對比.png| Serial | 復制 | 否 | 否 | 是 | 新生代 |
| ParNew | 復制 | 是 | 是 | 否 | 新生代 |
| Parallel Scavenge | 復制 | 是 | 是 | 否 | 新生代 |
| Serial Old | 標記-整理 | 否 | 否 | 是 | 老年代 |
| Parallel Old | 標記-整理 | 是 | 是 | 否 | 老年代 |
| CMS | 標記-清除 | 是 | 是 | 是 | 老年代 |
| G1 | 分代收集 | 是 | 是 | 否 | 老年代、新生代 |
| Serial | 單CPU,client模式 | 簡單高效,無線程切換開銷,專注GC | STW |
| ParNew | 多cpu,Server模式 | 并行并發GC | STW |
| Parallel Scavenge | 吞吐量優先,client或Server模式 | 吞吐量優先,設置吞吐量適應不同場景 | |
| Serial Old | 單CPU,client模式 | 簡單高效,無線程切換開銷,專注GC | STW |
| Parallel Old | 吞吐量優先,client或Server模式 | 吞吐量優先,設置吞吐量適應不同場景 | |
| CMS | 互聯網;B/S系統服務 | 并發收集,低停頓 | CPU資源敏感,無法處理浮動垃圾,存在內存碎片 |
| G1 | 面向服務端應用 | 并發并行,分代收集,停頓可測,空間整合 |
6、內存分配與回收策略
6.1、對象優先在Eden分配
大多數情況下,對象在Eden中分配。當Eden中沒有足夠的空間進行分配時,JVM將發起一次Minor GC。、
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為JAVA對象大多具備招生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現Major GC,通常會伴隨至少一次的Minor GC。Major GC速度一般比Minor GC的10倍以上。
6.2、大對象直接進入老年代
所謂的大對象是指,需要大量的連續內存空間的java對象,如很長的字符串及數組。大對象對虛擬機的內存分配來說就是一個壞消息(替 Java 虛擬機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。虛擬機提供了一個 -XX : PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配。這樣做的目的是避免在 Eden 區及兩個 Survivor 區之間發生大量的內存復制(復新生代采用復制算法收集內存)PretenureSizeThreshold參數只對Serial和ParNew兩種收集器有效。
6.3、長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡( Age )計數器。如果對象在 Eden 出生并經過第一次 Minor GG 后仍然存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并且對象年齡設為 1 。對象在 Survivor區每“熬過”一次 Minor GC ,年齡就增加1歲,當它的年齡增加到一定程度(默認為 15 歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數 -xx : MaxTenuringThreshold 設置。
6.4、動態對象年齡判定
為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果在 survivor空間中同年齡所有對象大小的總和大于 Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進人老年代,無須等到MaxTenuringThreshold中要求的年齡。
6.5、空間分配擔保
在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么 Minor GC 可以確保是安全的。如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC ,盡管這次 Minor GC 是有風險的:如果小于,或者HandlePromotionFailure 設置不允許冒險,那這時也要改為進行一次 Full GC 。
總結
以上是生活随笔為你收集整理的JVM(2)垃圾收集器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python函数和面向对象,小白看了都说
- 下一篇: 《超级演说家》刘媛媛:寒门贵子