JVM-垃圾收集入門

垃圾收集概述

對Java程序員而言不須要顯式地管理對象的生命週期:咱們能夠在須要時建立對象,對象再也不被使用時,會被JVM在後臺自動進行回收。那爲何咱們還要去了解GC和內存分配?java

答案很簡單:當須要排查各類內存溢出、內存泄露問題時或者當垃圾收集成爲系統達到更高併發量的瓶頸時,就須要對這些「自動化」的技術實施必要的監控和調節。程序員

垃圾收集器所關注的內存分配和回收的區域爲Java堆和方法區。一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立那些對象。算法

簡單來講,垃圾收集由兩步構成:查找再也不使用的對象(垃圾對象),以及釋放這些對象所管理的內存。服務器

查找再也不使用的對象

引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。多線程

客觀地說,引用計數算法的實現簡單,斷定效率也很高。可是,至少主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。併發

可達性分析算法

在主流的商用程序語言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱經過可達性分析來斷定對象是否存活的。這個算法的基本思路就是 經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的,將會被斷定爲可回收的對象。框架

在Java語言中,可做爲GC Roots的對象包括下面幾種:高併發

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

再談引用

不管經過哪一種算法查找再也不使用的對象,斷定對象是否存活都與「引用」有關。在JDK 1.2之前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹,可是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態。佈局

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

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

回收過程

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

若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個虛擬機自動創建的、低優先級的Finalizer線程去執行它。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身,只要從新與引用鏈上的任何一個對象創建關聯便可,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。

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

方法區回收

不少人認爲方法區(或者Hotspot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中不要求虛擬機在方法區實現垃圾收集,並且在方法區中進行垃圾收集的「性價比」通常比較低:在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。

永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是相似,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足3個條件才能算是「無用的類」:

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

虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制。

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

垃圾收集算法

標記-清除算法(Mark-Sweep)

最基礎的收集算法是「標記-清除」算法,該算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。它的主要不足有兩個:

  • 效率問題:標記和清除兩個過程的效率都不高

  • 空間問題:標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。標記—清除算法的執行過程以下圖所示:

    標記—清除算法的執行過程

複製算法

爲了解決效率問題,一種稱爲「複製(Copying)」的收集算法出現了,它將可用空間按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。這種算法的代價是將內存縮小爲原來的一半,未免過高了一點。複製算的執行過程以下圖所示:

複製算法

如今的商業虛擬機都採用這種收集算法來回收新生代,IBM公司的專門研究代表,新生代的對象98%是「朝生夕死」的,因此不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和用過的Survivor空間。當Survivor空間不夠用時,須要依賴其餘內存進行分配擔保。

標記-整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。因此在老年代通常不能直接選用這種算法。

根據老年代的特色,提出了「標記-整理」(Mark-Compat)算法,該算法的標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理端邊界之外的內存。"標記 - 整理"算法的示意圖以下圖:

標記-整理算法

分代收集算法

當前商業虛擬機的垃圾收集都採用「分代收集」算法,該算法根據對象存活週期的不一樣將內存劃分爲幾塊,這樣就能夠根據各個年代的特色採用最適當的收集算法。通常是把Java堆分爲"新生代(Young Generation)和"老年代(Old Generation或Tenured Generation)",新生代又被進一步劃分爲不一樣區域,分別稱爲Eden空間和Survivor空間。

在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收。

全部的垃圾收集算法在新生代進行垃圾回收時都存在「時空停頓」現象。全部應用線程都中止運行所產生的停頓稱爲時空停頓(stop-the-world)。一般這些停頓對應用的性能影響最大,調優垃圾收集時,儘可能減小這種停頓是最爲關鍵的考量因素。

Minor GC

新生代是堆的一部分,對象首先在新生代中分配。新生代填滿時,垃圾收集器會暫停全部的應用程序,回收新生代空間。再也不使用的對象會被回收,仍然在使用的對象會被移動到其餘地方。這種操做被稱爲Minor GC。

Full GC

對象不斷地被移動到老年代,最終老年代也會被填滿,JVM須要找到老年代中再也不使用的對象,並對它們進行回收。這個過程被稱爲Full GC,一般致使應用程序長時間的停頓。

垃圾收集器

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

img

Serial收集器

Serial收集器是最基本、發展歷史最悠久的收集器,在JDK 1.3以前是虛擬機新生代收集的惟一選擇。該收集器使用單線程清理堆的內容。不管是進行Minor GC仍是Full GC,在進行清理堆空間時,全部的應用線程都會被暫停(Stop The World),直到它收集結束。進行Full GC時,它還會對老年代空間的對象進行壓縮整理。

Serial收集器是虛擬機運行在Client模式下的默認新生代收集器。也有着優於其餘收集器的地方:

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

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數、收集算法、「Stop The World」、對象分配規則、回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。

ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做。CMS做爲老年代的收集器,新生代只能選擇ParNew或者Serial收集器中的一個。ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可使用-XX:+UseParNewGC選項來強制指定它。

ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越Serial收集器。固然,隨着可使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU很是多的環境下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

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

併發(Concurrent):指用戶線程與垃圾收集線程同時執行,用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,也是使用複製算法的收集器,又是並行的多線程收集器。可是Parallel Scavenge收集器的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算任務,主要適合在後臺運算而不須要太多交互的任務。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

  • MaxGCPauseMillis:MaxGCPauseMillis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超過設定值。不要認爲若是把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代確定比收集500MB快,這就致使垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,如今變成5秒收集一次、每次停頓70毫秒。停頓時間的確在降低,但吞吐量也降下來了。
  • GCTimeRatio:GCTimeRatio參數的值應當是(0,100)之間的整數,默認值爲99,是垃圾收集時間佔總時間的比率,至關於吞吐量的倒數。若是把此參數設置爲19,那容許的最大GC時間就佔總時間的5%(即1 / (1+19))。

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

Serial Old收集器

Serial Old是Serial收集器的老年代版本,一樣是一個單線程收集器,使用「標記-整理」算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下 ,那麼它主要還有兩大用途:

  • 在JDK 1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
  • 做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器是在JDK 1.6中才開始提供的。在此以前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。緣由是若是新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇。

Parallel Old收集器出現後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。CMS收集器是基於「標記-清除」算法實現的,它的運做過程相對於前面幾種收集器來講更復雜一些,整個過程分爲4個步驟:

  • 初始標記(CMS initial mark):標記一下GC Roots能直接關聯到的對象,速度很快。
  • 併發標記(CMS concurrent mark):進行GC Roots Tracing的過程
  • 從新標記(CMS remark):爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
  • 併發清除(CMS concurrent sweep)

其中,初始標記、從新標記這兩個步驟仍然須要「Stop The World」。整個過程當中耗時最長的併發標記和併發清楚過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

CMS收集器也存在以下3個明顯的缺點:

  1. 對CPU資源很是敏感

    面向併發設計的程序都對CPU資源比較敏感。CMS收集器在併發階段,雖然不會致使用戶線程停頓,但會由於佔用了一部分線程(或者說CPU資源)而致使應用程序變慢,總吞吐量會下降。CMS默認啓動的回收線程數是(CPU數量 + 3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不小於25%的CPU資源,而且隨着CPU數量的增長而降低。當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,若是原本CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了50%,其實也讓人沒法接受。

  2. 沒法處理浮動垃圾(Floating Garbage),可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。

    CMS在併發清理階段用戶線程還須要運行着,伴隨程序運行天然就會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲「浮動垃圾」。因爲在垃圾收集階段用戶線程還須要運行,那就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能其餘收集器那樣等到老年代幾乎徹底被填滿了在進行收集,須要預留一部分空間提供併發收集時的程序運做使用。

    若是CMS運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。

    老年代使用了多少內存時纔會觸發CMS收集器,是由參數:-XX:CMSInitiatingOccupancyFraction的值決定的。若是參數CMSInitiatingOccupancyFraction的值設置太高很容易致使大量「Concurrent Mode Failure」失敗,性能反而下降。

  3. 產生空間碎片

    CMS是一款基於「標記-清除」算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,每每會出現老年代還有很大空間剩餘,但沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次Full GC。

    爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開發參數(默認開啓),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間變長了。另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。

G1收集器

G1 GC,全稱Garbage-First Garbage Collector,經過-XX:+UseG1GC參數來啓用,在JDK 7u4版本發行時被正式推出。在JDK 9中,G1被提議設置爲默認垃圾收集器(JEP 248)。

G1是一種服務器端的垃圾收集器,G1收集器的設計目標是取代CMS收集器。與CMS相比,在如下方面表現的更出色:

  • G1是一個有整理內存過程的垃圾收集器,不會產生不少內存碎片。
  • G1的Stop The World(STW)更可控,G1在停頓時間上添加了預測機制,用戶能夠指按期望停頓時間。

Region

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

在上圖中,注意到有一些Region標明瞭H,它表明Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大於等於region一半的對象。

可預測的停頓時間模型

G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大Region(這也就是Garbage-First名稱的來由)。

這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限時間內能夠得到儘量高的收集效率。

Remembered Set

Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?Region不多是孤立的,一個對象分配在某個Region中,它並不是只能被本Region中的其餘對象引用,而是能夠與整個Java堆任意的對象發生引用關係。那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準備性。這個問題其實並不是在G1中才有,只是在G1中更加突出而已。

在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中,若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set即保證不對全堆掃描也不會有遺漏。

相關文章
相關標籤/搜索