深刻理解JVM以內存回收機制

背景

C、C++等語言中,內存的分配和釋放由程序代碼來完成,容易出現因爲程序員漏寫內存釋放代碼引發的內存泄露,最終致使系統內存耗盡。
Java代碼運行在JVM中,由JVM來管理 堆Heap 內存的分配和回收(Garbage Collection),把程序員從繁瑣的內存管理工做中釋放出來,更專一於業務開發。Java內存回收工做由標記(識別可回收對象)和回收(釋放可回收對象)兩個步驟組成。
和程序代碼釋放內存相比,內存自動管理會佔用一部分CPU時間,Stop The World特色回暫停業務程序運行,很是影響執行效率。Java各版本中,一直致力於內存管理算法的優化,造成了一套針對各類內存分區(新生代、老年代)和運行場景(單核、多核、客戶端、服務端)的特色而針對性設計的內存回收算法。程序員

這裏說的內存回收機制,主要是指針對 堆Heap元空間Metaspace內存的回收,線程相關內存(棧、本地棧、程序計數器)內存隨線程建立和回收,直接內存的釋放由其在堆內存中引用釋放時觸發。

內存標記算法

在內存被回收前,系統必須標記哪些內存已經沒有人使用能夠釋放,這個工做就由內存標記算法的來完成,在Java各版本中,使用過以下幾種標記算法。算法

引用計數法(Reference Counting Collector)

這是早期的內存標記算法,每一個堆中分配的對象都有一個引用計數器,計數一個對象被引用的次數。當對象建立並賦值給變量時,計數爲1,當有其餘變量引用該對象時,引用計數+1;但引用此對象的變量超出存活範圍或釋放對對象引用(包括變量引用了其餘對象或變量被設置爲null等),引用計數-1。對象的引用計數爲0時,表示此對象可被垃圾收集器回收。
引用計數法的有點是簡單,執行速度快,只要變量一遍對象檢測引用計數是否爲0便可判斷是否可回收;肯定是沒法檢測出循環引用而致使內存沒法回收。多線程

跟蹤算法(Tracing Collector)

採用跟搜索算法,搜索算法引入了圖論,把全部對象間的關係當作一張圖,內存標記從一組根節點(GC Root Set)開始,經過遞歸搜索,創建對象的引用關係圖,當搜索完畢後,圖外的對象就是可回收對象。這是目前Java中使用的內存標記算法
GC Root Set.PNG併發

可做爲GC Root的對象包括框架

  1. 棧中局部變量引用的對象
  2. 類靜態變量引用的對象
  3. 常量引用的變量
  4. 本地方法棧引用的對象

內存回收方式

標記-清理算法(Mark and Sweep)

採用跟蹤算法標記內存對象後,再掃描堆內存中未被標記的對象,進行回收。此算法不移動對象,僅對不存活對象進行回收,在存活對象佔比高的狀況下處理效率高,但不移動對象會引發內存碎片。性能

標記-整理算法(Compacting)

此方法和標記-清理算法使用相同標記算法,但在對不存活對象回收時,會把存活對象向內存前部空閒區域移動,同時更新對象的指針。此方法在清理的基礎上,會對對象進行移動,執行成本較高,但可解決內存碎片問題。基於此算法的內存回收實現,通常會增長句柄和句柄表。優化

複製-清除算法(Copying)

該算法把內存分爲空閒區和對象區,新建對象存儲到對象區中。當對象區滿時,先採用跟蹤算法對對象進行標記,再把存活對象拷貝到空閒區,清空原對象區,空閒區和對象區互換角色。在拷貝過車中,程序須要暫停,此算法適用於存活對象叫少的狀況,能夠解決內存碎片問題。spa

分代回收策略

JDK8中,堆中移除了永生代區域,堆內存主要由新生代老年代兩部分組成。其中新生代由一個伊甸園(Eden)和兩個倖存者Survivor From和Survivor To 3部分組成,新建立對象首先保存在Eden中,當Eden中對象達到必定數量時,JVM觸發Minor GC,GC時,先把Eden和From中的存活對象拷貝到Survivor To區,再清除Eden和From兩個區域的數據,最後From和To互換身份,完成一次內存回收。新生代區域對象數量大,存活時間短,通常採用複製-清除算法,經過這種結構和回收方式來提升垃圾回收效率,減小內存碎片。
通過若干(默認15)次後還存活的對象,將進入老年代區,當老年代數據滿時,會觸發Major GC(又稱Full GC),此時新生代、老年代、元區域、直接內存區域都會執行GC操做。
JVM堆信息.PNG
老年代:新生代的內存大小默認比例爲2:1。Eden和兩個Survivor的比例爲8:1:1。線程

垃圾收集器

上述的內存標記算法、回收方式和分代策略是垃圾回收的方法,根據這些方法,針對不一樣的用戶場景(Server、Client)和系統配置(單線程、多線程),JVM實現了適用於各場景的垃圾回收器。設計

  1. 年輕代收集器

    • Serial(複製-清除)
    • ParNew(複製-清除)
    • Parallel Scavenge(複製-清除)
  2. 老年代收集器

    • Serial Old(標記-整理)
    • Parallel Old(標記-整理)
    • CMS(Concurrent Mark Sweep)(標記-清理)
  3. 混合收集器

    • G1(標記-整理)應用於整個堆

年輕代收集器

Serial(複製-清除)

Serial是單線程收集器,Serial收集器只能使用單個線程進行收集工做,在收集的時候必須得停掉其它線程,等待收集工做完成其它線程才能夠繼續工做。
Serial收集器是JVM中最先的垃圾收集器,也是JDK1.3前的惟一收集器,再也不適用於現代多核CPU和Server(服務端)場景,可是很是的適合單核CPU和Client場景。
Serial GC.PNG

ParNew(複製-清除)

ParNew是Serial的升級版,其工做的流程和Serial基本一致,主要的改進是支持多線程同時執行垃圾回收工做,即上圖中的GC Thread支持多線程,能夠充分利用多核CPU的性能。它是HotSpot上第一個真正意義實現併發的收集器。GC默認開啓線程數等於CPU數量,可經過 -XX:ParallelGCThreads 來控制垃圾收集線程的數量。

Parallel Scavenge(複製-清除)

Parallel Scavenge是吞吐量優先的收集器,其工做方式和ParNew基本同樣,可是它以提升系統吞吐量(Throughput)爲設計目標,吞吐量=業務運行時間/系統總運行(業務+GC)時間。
ParNew等收集器的關注點是儘可能縮小垃圾回收的停頓時間,而縮短停頓時間必然須要提升垃圾回收的頻率,致使業務線程和GC線程間頻繁的切換,從而增長CPU在現場切換上的損耗。
而以吞吐量爲設計目標的Parallel Scavenge收集器,能夠經過擴大新生代內存容量,減小垃圾回收發生的次數,雖然提升了單次GC的時長,但減小了線程切換開銷,從總體上能夠提升系統的吞吐量。
Parallel Scavenge GC.PNG

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

參數 做用 說明
-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間 單次GC的最大毫秒數
-XX:GCTimeRatio 設置吞吐量大小 業務:GC時間比例,默認爲99,即GC時間佔比爲 1/(1+99)=1%

單次GC時間參數並不是設置的越小越好,而是一把雙刃劍,若是減小單次GC時間,必然致使GC頻率的上升;而設置的增大,則必然須要更大的內存來支撐。

因爲Parallel Scavenge和其餘收集器(Serial、ParNew、CMS等)使用了不用的設計框架,致使其沒法和CMS協同工做。

老年代收集器

Serial Old(標記-整理)

工做模式基本和新生代的Serial同樣爲單線程,它採用標記-整理算法,這個模式主要是給Client模式下的JVM使用。若是是Server模式有兩大用途:

  1. JDK5前和Parallel Scavenge搭配使用,JDK5前也只有這個老年代收集器能夠和它搭配
  2. 做爲CMS收集器的後備

Parallel Old(標記-整理)

Parallel Scavenge的老年版本,JDK6開始出現,採用標記-整理算法。Parallel Old的出現結合Parallel Scavenge,真正的造成「吞吐量優先」的收集器組合。JDK7和8中,做爲老年代默認的收集器。

在JDK6之前,新生代的Parallel Scavenge只能和Serial Old配合使用,而Serial Old爲單線程,Server模式下沒法充分利用多核CPU,這種組合沒法讓應用的吞吐量最大化。

CMS(Concurrent Mark Sweep)(標記-清理)

CMS收集器是以最短回收停頓時間爲目標的收集器。重視響應,以帶來好的用戶體驗,是併發低停頓收集器,經過-XX:+UseConcMarkSweepGC參數啓用CMS收集器。
CMS採用支撐多線程併發的標記-清除算法,它的運做分爲4個階段:

  1. 初始標記(Initial Mark)
    標記GC Root Set直接關聯的對象
  2. 併發標記(Concurrent Mark)
    以初始標記對象爲基礎,併發標記其關聯的對象,直到全部對象標記完成,標記進程和用戶進程併發執行。
  3. 從新標記(Remark)
    爲了修正因併發標記期間用戶程序運行而產生變更的那一部分對象的標記記錄,暫停用戶進程對這部分對象從新標記。
  4. 併發清除(Sweep)
    將前面標記對象的內存回收,這個階段GC線程與用戶線程併發運行。

CMS GC.PNG
CMS在初始標記和從新標記階段須要暫停業務線程,在執行時間上,初始標記 < 從新標記 < 併發標記,因此時間最長的併發標記,業務線程和GC線程併發運行,因此用戶感覺上,GC暫停的時間很短。但其也存在幾個缺點,具體以下:

  1. CMS默認配置啓動的時候垃圾線程數爲 (CPU數+3)/4,性能很容易受CPU核數影響
  2. CMS沒法處理浮動垃圾,可能致使Concurrent Mode Failure(併發模式故障)而觸發Full GC
  3. CMS採用標記-清除算法,存在垃圾碎片的問題
爲了解決CMS致使的內存碎片問題,CMS模式提供了 -XX:+UseCMSCompactAtFullCollection 選項,選項默認開啓,用於CMS要進行Full GC時進行內存碎片整理,因爲內存整理的過程沒法併發,須要中止業務進程,因此啓這個選項會影響性能。
相關文章
相關標籤/搜索