JVM虛擬機內存分配、GC垃圾回收機制原理、垃圾收集器

I. 怎樣分配- JVM內存分配策略

對象內存主要分配在新生代Eden, 如果啓用了本地線程分配緩衝, 優先在TLAB上分配, 少數情況能會直接分配在老年代, 或被拆分成標量類型在棧上分配(JIT優化). 分配的規則並不是百分百固定, 細節主要取決於垃圾收集器組合, 以及VM內存相關的參數.


對象分配

  • 優先在Eden區分配
    JVM內存模型一文中, 我們大致瞭解了VM年輕代堆內存可以劃分爲一塊Eden區和兩塊Survivor. 在大多數情況下, 對象在新生代Eden區中分配, Eden區沒有足夠空間分配時, VM發起一次Minor GC, Eden區和其中一塊Survivor區內尚存活的對象放入另一塊Survivor區域, 如果在Minor GC期間發現新生代存活對象無法放入空閒的Survivor, 則會通過空間分配擔保機制使對象提前進入老年代(空間分配擔保見下).
  • 大對象直接進入老年代
    SerialParNew兩款收集器提供了-XX:PretenureSizeThreshold的參數, 令大於該值的大對象直接在老年代分配, 這樣做的目的是避免在Eden區和Survivor區之間產生大量的內存複製(大對象一般指 需要大量連續內存的Java對象, 如很長的字符串和數組), 因此大對象容易導致還有不少空閒內存就提前觸發GC以獲取足夠的連續空間.

對象晉升

  • 年齡閾值
    VM爲每個對象定義了一個對象年齡(Age)計數器, 對象在Eden出生如果經第一次Minor GC後仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 並將年齡設爲1. 以後對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增加到一定程度(-XX:MaxTenuringThreshold, 默認15), 將會晉升到老年代.
  • 提前晉升: 動態年齡判定
    然而VM並不總是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就可以直接進入老年代, 而無須等到晉升年齡.

II. 何時回收-對象生死判定

(哪些內存需要回收/何時回收)

在堆裏面存放着Java世界中幾乎所有的對象實例, 垃圾收集器在對堆進行回收前, 第一件事就是判斷哪些對象已死(可回收).


可達性分析算法

在主流商用語言(JavaC#)的主流實現中, 都是通過可達性分析算法來判定對象是否存活的: 通過一系列的稱爲 GC Roots 的對象作爲起點, 然後向下搜索; 搜索所走過的路徑稱爲引用鏈/Reference Chain, 當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的, 如下圖Object567 雖然互有關聯, 但它們到GC Roots是不可達的, 因此也會被判定爲可回收的對象:

  • Java, 可作爲GC Roots的對象包括:
    1. 方法區: 類靜態屬性引用的對象;
    2. 方法區: 常量引用的對象;
    3. 虛擬機棧(本地變量表)中引用的對象.
    4. 本地方法棧JNI(Native方法)中引用的對象。

注: 即使在可達性分析算法中不可達的對象, VM也並不是馬上對其回收, 因爲要真正宣告一個對象死亡, 至少要經歷兩次標記過程: 第一次是在可達性分析後發現沒有與GC Roots相連接的引用鏈, 第二次是GC對在F-Queue執行隊列中的對象進行的小規模標記(對象需要覆蓋finalize()方法且沒被調用過).


III. GC原理- 垃圾收集算法

分代收集算法 VS 分區收集算法

  • 分代收集
    當前主流VM垃圾收集都採用分代收集”(Generational Collection)算法, 這種算法會根據對象存活週期的不同將內存劃分爲幾塊, JVM中的 新生代老年代永久代. 這樣就可以根據各年代特點分別採用最適當的GC算法:
    • 在新生代: 每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用複製算法只需要付出少量存活對象的複製成本就可以完成收集.
    • 在老年代: 因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用標記清理標記整理算法來進行回收不必進行內存複製, 且直接騰出空閒內存.
  • 分區收集
    上面介紹的分代收集算法是將對象的生命週期按長短劃分爲兩個部分, 而分區算法則將整個堆空間劃分爲連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的好處是可以控制一次回收多少個小區間.
    在相同條件下, 堆空間越大, 一次GC耗時就越長, 從而產生的停頓也越長. 爲了更好地控制GC產生的停頓時間, 將一塊大的內存區域分割爲多個小塊, 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減少一次GC所產生的停頓.

分代收集

新生代-複製算法

該算法的核心是將可用內存按容量劃分爲大小相等的兩塊, 每次只用其中一塊, 當這一塊的內存用完, 就將還存活的對象複製到另外一塊上面, 然後把已使用過的內存空間一次清理掉.

(圖片來源jvm垃圾收集算法)

這使得每次只對其中一塊內存進行回收, 分配也就不用考慮內存碎片等複雜情況, 實現簡單且運行高效.

現代商用VM的新生代均採用複製算法, 但由於新生代中的98%的對象都是生存週期極短的, 因此並不需完全按照1∶1的比例劃分新生代空間, 而是將新生代劃分爲一塊較大的Eden區和兩塊較小的Survivor(HotSpot默認EdenSurvivor的大小比例爲8∶1), 每次只用Eden和其中一塊Survivor. 當發生MinorGC, EdenSurvivor中還存活着的對象一次性地拷貝到另外一塊Survivor, 最後清理掉Eden和剛纔用過的Survivor的空間. Survivor空間不夠用(不足以保存尚存活的對象), 需要依賴老年代進行空間分配擔保機制, 這部分內存直接進入老年代.


老年代-標記清除算法

該算法分爲標記清除兩個階段首先標記出所有需要回收的對象(可達性分析), 在標記完成後統一清理掉所有被標記的對象.

該算法會有以下兩個問題:
1.
效率問題: 標記和清除過程的效率都不高;
2.
空間問題: 標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集.


老年代-標記整理算法

標記清除算法會產生內存碎片問題, 而複製算法需要有額外的內存擔保空間, 於是針對老年代的特點, 又有了標記整理算法. 標記整理算法的標記過程與標記清除算法相同, 但後續步驟不再對可回收對象直接清理, 而是讓所有存活的對象都向一端移動,然後清理掉端邊界以外的內存.


永久代-方法區回收

  • 在方法區進行垃圾回收一般性價比較低, 因爲在方法區主要回收兩部分內容廢棄常量無用的類. 回收廢棄常量與回收其他年代中的對象類似, 但要判斷一個類是否無用則條件相當苛刻:
    1. 該類所有的實例都已經被回收, Java堆中不存在該類的任何實例;
    2. 該類對應的Class對象沒有在任何地方被引用(也就是在任何地方都無法通過反射訪問該類的方法);
    3. 加載該類的ClassLoader已經被回收.

但即使滿足以上條件也未必一定會回收, Hotspot VM還提供了-Xnoclassgc參數控制(關閉CLASS的垃圾回收功能). 因此在大量使用動態代理、CGLib等字節碼框架的應用中一定要關閉該選項, 開啓VM的類卸載功能, 以保證方法區不會溢出.


補充: 空間分配擔保

在執行Minor GC, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 由於新生代使用複製收集算法, 爲了提升內存利用率, 只使用了其中一個Survivor作爲輪換備份, 因此當出現大量對象在Minor GC後仍然存活的情況時, 就需要老年代進行分配擔保, Survivor無法容納的對象直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是無法明確知道的, 因此Minor GC, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間).
然而取歷次晉升的對象的平均大小也是有一定風險的, 如果某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能導致擔保失敗(Handle Promotion Failure, 老年代也無法存放這些對象了), 此時就只好在失敗後重新發起一次Full GC(讓老年代騰出更多空間).


IX. GC實現- 垃圾收集器

GC實現目標: 準確、高效、低停頓、空閒內存規整.


新生代

1. Serial收集器

Serial收集器是Hotspot運行在Client模式下的默認新生代收集器, 它的特點是 只用一個CPU/一條收集線程去完成GC工作, 且在進行垃圾收集時必須暫停其他所有的工作線程(“Stop The World” -後面簡稱STW).

雖然是單線程收集, 但它卻簡單而高效, VM管理內存不大的情況下(收集幾十M~一兩百M的新生代), 停頓時間完全可以控制在幾十毫秒~一百多毫秒內.


2. ParNew收集器

ParNew收集器其實是前面Serial的多線程版本, 使用多條線程進行GC, 包括Serial可用的所有控制參數、收集算法、STW、對象分配規則、回收策略等都與Serial完全一樣(也是VM啓用CMS收集器-XX: +UseConcMarkSweepGC的默認新生代收集器).

由於存在線程切換的開銷, ParNew在單CPU的環境中比不上Serial, 且在通過超線程技術實現的兩個CPU的環境中也不能100%保證能超越Serial. 但隨着可用的CPU數量的增加, 收集效率肯定也會大大增加(ParNew收集線程數與CPU的數量相同, 因此在CPU數量過大的環境中, 可用-XX:ParallelGCThreads參數控制GC線程數).


3. Parallel Scavenge收集器

ParNew類似, Parallel Scavenge也是使用複製算法, 也是並行多線程收集器. 但與其他收集器關注儘可能縮短垃圾收集時間不同, Parallel Scavenge更關注系統吞吐量:

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

停頓時間越短就越適用於用戶交互的程序-良好的響應速度能提升用戶的體驗;而高吞吐量則適用於後臺運算而不需要太多交互的任務-可以最高效率地利用CPU時間,儘快地完成程序的運算任務. Parallel Scavenge提供瞭如下參數設置系統吞吐量:

Parallel Scavenge參數

描述

MaxGCPauseMillis

(毫秒數) 收集器將盡力保證內存回收花費的時間不超過設定值, 但如果太小將會導致GC的頻率增加.

GCTimeRatio

(整數:0 < GCTimeRatio < 100) 是垃圾收集時間佔總時間的比率

-XX:+UseAdaptiveSizePolicy

啓用GC自適應的調節策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等細節參數, VM會根據當前系統的運行情況收集性能監控信息, 動態調整這些參數以提供最合適的停頓時間或最大的吞吐量


老年代

Serial Old收集器

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

  • Serial Old應用場景如下:
    • JDK 1.5之前與Parallel Scavenge收集器搭配使用;
    • 作爲CMS收集器的後備預案, 在併發收集發生Concurrent Mode Failure時啓用(見下:CMS收集器).

Parallel Old收集器

Parallel OldParallel Scavenge收老年代版本, 使用多線程和標記-整理算法, 吞吐量優先, 主要與Parallel Scavenge配合在 注重吞吐量  CPU資源敏感 系統內使用:


CMS收集器

CMS(Concurrent Mark Sweep)收集器是一款具有劃時代意義的收集器, 一款真正意義上的併發收集器, 雖然現在已經有了理論意義上表現更好的G1收集器, 但現在主流互聯網企業線上選用的仍是CMS(Taobao、微店).
CMS
是一種以獲取最短回收停頓時間爲目標的收集器(CMS又稱多併發低暫停的收集器), 基於標記-清除算法實現, 整個GC過程分爲以下4個步驟:
1. 
初始標記(CMS initial mark)
2.
併發標記(CMS concurrent mark: GC Roots Tracing過程)
3. 
重新標記(CMS remark)
4.
併發清除(CMS concurrent sweep: 已死象將會就地釋放, 注意此處沒有壓縮)
其中兩個加粗的步驟(初始標記重新標記)仍需STW. 但初始標記僅只標記一下GC Roots能直接關聯到的對象, 速度很快; 而重新標記則是爲了修正併發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄, 雖然一般比初始標記階段稍長, 但要遠小於併發標記時間.

(由於整個GC過程耗時最長的併發標記和併發清除階段的GC線程可與用戶線程一起工作, 所以總體上CMSGC過程是與用戶線程一起併發地執行的.

由於CMS收集器將整個GC過程進行了更細粒度的劃分, 因此可以實現併發收集、低停頓的優勢, 但它也並非十分完美, 其存在缺點及解決策略如下:

  1.  

CMS默認啓動的回收線程數=(CPU數目+3)4

  1.  

CPU>4, GC線程最多佔用不超過25%CPU資源, 但是當CPU<=4, GC線程可能就會過多的佔用用戶CPU資源, 從而導致應用程序變慢, 總吞吐量降低.

  1.  
  2. 無法處理浮動垃圾, 可能出現Promotion FailureConcurrent Mode Failure而導致另一次Full GC的產生: 浮動垃圾是指在CMS併發清理階段用戶線程運行而產生的新垃圾. 由於在GC階段用戶線程還需運行, 因此還需要預留足夠的內存空間給用戶線程使用, 導致CMS不能像其他收集器那樣等到老年代幾乎填滿了再進行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction參數來設置GC的觸發百分比(以及-XX:+UseCMSInitiatingOccupancyOnly來啓用該觸發百分比), 當老年代的使用空間超過該比例後CMS就會被觸發(JDK 1.6之後默認92%). 但當CMS運行期間預留的內存無法滿足程序需要, 就會出現上述Promotion Failure等失敗, 這時VM將啓動後備預案: 臨時啓用Serial Old收集器來重新執行Full GC(CMS通常配合大內存使用, 一旦大內存轉入串行的Serial GC, 那停頓的時間就是大家都不願看到的了).
  3. 最後, 由於CMS採用標記-清除算法實現, 可能會產生大量內存碎片. 內存碎片過多可能會導致無法分配大對象而提前觸發Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection開關參數, 用於在Full GC後再執行一個碎片整理過程. 但內存整理是無法併發的, 內存碎片問題雖然沒有了, 但停頓時間也因此變長了, 因此CMS還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction用於設置在執行N次不進行內存整理的Full GC, 跟着來一次帶整理的(默認爲0: 每次進入Full GC時都進行碎片整理).

分區收集- G1收集器

G1(Garbage-First)是一款面向服務端應用的收集器, 主要目標用於配備多顆CPU的服務器治理大內存.
- G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
-XX:+UseG1GC 啓用G1收集器.

與其他基於分代的收集器不同, G1將整個Java堆劃分爲多個大小相等的獨立區域(Region), 雖然還保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔離的了, 它們都是一部分Region(不需要連續)的集合.

每塊區域既有可能屬於O區、也有可能是Y, 因此不需要一次就對整個老年代/新生代回收. 而是當線程併發尋找可回收的對象時, 有些區塊包含可回收的對象要比其他區塊多很多. 雖然在清理這些區塊時G1仍然需要暫停應用線程, 但可以用相對較少的時間優先回收垃圾較多的Region(這也是G1命名的來源). 這種方式保證了G1可以在有限的時間內獲取儘可能高的收集效率.


新生代收集

G1的新生代收集跟ParNew類似: 存活的對象被轉移到一個/多個Survivor Regions. 如果存活時間達到閥值, 這部分對象就會被提升到老年代.

  • G1的新生代收集特點如下:
    • 一整塊堆內存被分爲多個Regions.
    • 存活對象被拷貝到新的Survivor區或老年代.
    • 年輕代內存由一組不連續的heap區組成, 這種方法使得可以動態調整各代區域尺寸.
    • Young GCs會有STW事件, 進行時所有應用程序線程都會被暫停.
    • 多線程併發GC.

老年代收集

G1老年代GC會執行以下階段:

注: 一下有些階段也是年輕代垃圾收集的一部分.

index

Phase

Description

(1)

初始標記 (Initial Mark: Stop the World Event)

在G1中, 該操作附着一次年輕代GC, 以標記Survivor中有可能引用到老年代對象的Regions.

(2)

掃描根區域 (Root Region Scanning: 與應用程序併發執行)

掃描Survivor中能夠引用到老年代的references. 但必須在Minor GC觸發前執行完.

(3)

併發標記 (Concurrent Marking : 與應用程序併發執行)

在整個堆中查找存活對象, 但該階段可能會被Minor GC中斷.

(4)

重新標記 (Remark : Stop the World Event)

完成堆內存中存活對象的標記. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除並回收, 並計算所有區域的活躍度).

(5)

清理 (Cleanup : Stop the World Event and Concurrent)

見下 5-1、2、3

 

5-1 (Stop the world)

在含有存活對象和完全空閒的區域上進行統計

 

5-2 (Stop the world)

擦除Remembered Sets.

 

5-3 (Concurrent)

重置空regions並將他們返還給空閒列表(free list)

(*)

Copying/Cleanup (Stop the World Event)

選擇”活躍度”最低的區域(這些區域可以最快的完成回收). 拷貝/轉移存活的對象到新的尚未使用的regions. 該階段會被記錄在gc-log內(只發生年輕代[GC pause (young)], 與老年代一起執行則被記錄爲[GC Pause (mixed)].

詳細步驟可參考 Oracle官方文檔-The G1 Garbage Collector Step by Step.

  • G1老年代GC特點如下:
    • 併發標記階段(index 3)
      1. 在與應用程序併發執行的過程中會計算活躍度信息.
      2. 這些活躍度信息標識出那些regions最適合在STW期間回收(which regions will be best to reclaim during an evacuation pause).
      3. 不像CMS有清理階段.
    • 再次標記階段(index 4)
      1. 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.
      2. region直接被回收.
    • 拷貝/清理階段(Copying/Cleanup Phase)
      • 年輕代與老年代同時回收.
      • 老年代內存回收會基於他的活躍度信息.

補充: 關於Remembered Set

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


V. JVM小工具

${JAVA_HOME}/bin/目錄下Sun/Oracle給我們提供了一些處理應用程序性能問題、定位故障的工具, 包含

bin

描述

功能

jps

打印Hotspot VM進程

VMID、JVM參數、main()函數參數、主類名/Jar路徑

jstat

查看Hotspot VM 運行時信息

類加載、內存、GC[可分代查看]、JIT編譯

jinfo

查看和修改虛擬機各項配置

-flag name=value

jmap

heapdump: 生成VM堆轉儲快照、查詢finalize執行隊列、Java堆和永久代詳細信息

jmap -dump:live,format=b,file=heap.bin [VMID]

jstack

查看VM當前時刻的線程快照: 當前VM內每一條線程正在執行的方法堆棧集合

Thread.getAllStackTraces()提供了類似的功能

javap

查看經javac之後產生的JVM字節碼代碼

自動解析.class文件, 避免了去理解class文件格式以及手動解析class文件內容

jcmd

一個多功能工具, 可以用來導出堆, 查看Java進程、導出線程信息、 執行GC、查看性能相關數據等

幾乎集合了jps、jstat、jinfo、jmap、jstack所有功能

jconsole

基於JMX的可視化監視、管理工具

可以查看內存、線程、類、CPU信息, 以及對JMX MBean進行管理

jvisualvm

JDK中最強大運行監視和故障處理工具

可以監控內存泄露、跟蹤垃圾回收、執行時內存分析、CPU分析、線程分析…


VI. VM常用參數整理

參數

描述

-Xms

最小堆大小

-Xmx

最大堆大小

-Xmn

新生代大小

-XX:PermSize

永久代大小

-XX:MaxPermSize

永久代最大大小

-XX:+PrintGC

輸出GC日誌

-verbose:gc

-

-XX:+PrintGCDetails

輸出GC的詳細日誌

-XX:+PrintGCTimeStamps

輸出GC時間戳(以基準時間的形式)

-XX:+PrintHeapAtGC

在進行GC的前後打印出堆的信息

-Xloggc:/path/gc.log

日誌文件的輸出路徑

-XX:+PrintGCApplicationStoppedTime

打印由GC產生的停頓時間

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章