【jvm】java垃圾回收

Java的一大特性就是內存的分配和回收都是自動進行的。當程序規模不大時,咱們徹底能夠不考慮內存的使用狀況。可是一旦程序的規模足夠大,對性能的要求足夠高時,瞭解Java垃圾收集(GC)的內部機制並根據具體的應用特徵來調整使用的垃圾收集算法就顯得十分重要了。程序員

GC屬性

吞吐量(Throughput):程序運行時間 /(程序運行時間 + 垃圾收集時間) 
延遲(Latency):使程序儘量少的由於垃圾回收而暫停的能力 
足跡(Footprint):GC運行時使用的內存空間 
敏捷度(Promptness):對象被標記爲死亡到對象所佔內存被回收所經歷的時間算法

影響GC的因素

堆大小 
堆內對象的存活率 
內存分配速率 
引用更新速率 
對象的壽命編程

GC步驟

Mark

標記階段,目的是將不可用的對象標記出來,以便進行後階段的回收。那麼,如何判斷一個對象是否可用呢?這跟指向該對象的引用有很大的關係。所以,在具體研究對象可用性斷定算法以前,讓咱們先看一看Java中不一樣的引用類型。安全

Java引用類型服務器

Java中主要有如下四種引用類型:多線程

  • 強引用(Strong Reference):大多數狀況下使用的引用類型。如Object obj = new Object();中的obj就屬於強引用。被強引用引用的對象在任什麼時候候都不能被回收。
  • 軟引用(Soft Reference):使用SoftReference類建立的引用,其所引用的對象將在內存空間不足時被回收。
  • 弱引用(Weak Reference):使用WeakReference類建立的引用,無論內存空間是否足夠,其所引用的對象都將在下一次GC時被回收。
  • 虛引用(Phantom Reference):使用PhantomReference類建立的引用,不影響其所引用的對象的壽命,僅在被回收時收到一個系統通知 
    上述四種引用的強度由上至下依次減弱。能夠看出,除了強引用,其餘引用對於GC的執行並沒有太大的影響。所以,如下討論中談到的引用均指強引用。

引用計數算法(Reference Counting)

該算法給每一個對象添加一個引用計數器,當有引用指向對象時,計數器加1,當引用失效時,計數器減1。所以,當一個對象的引用計數變爲0時,就證實該對象不可用,其所佔用的內存也能夠當即被釋放。架構

可是主流的Java虛擬機中並無使用這一簡單高效的算法來管理內存,主要緣由就是它沒法解決循環引用(Circular)的問題。也即,當對象A和對象B相互引用,而沒有任何其餘對象指向A和B時,因爲A和B的引用計數均爲1(不等於0),引用計數算法將沒法回收這兩個對象。併發

同時,該算法對引用計數的頻繁更新也會使得效率下降。jvm

可達性分析算法(Reachability Analysis)

從一系列名爲」GC Roots」的對象開始向下搜索,就能夠造成若干條引用鏈。若是一個對象到」GC Roots」無任何引用鏈相連,該對象則被斷定爲可回收對象。工具

能夠做爲GC Roots的對象包括如下幾類:

  • 虛擬機棧中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • Native方法引用的對象 
    該算法能夠很好的解決循環引用的問題。同時,對於高度變化的程序來講比引用計數法效率更高。可是,在迅速發現不可用的對象方面,則沒有引用計數法那麼快。

Clean Up

清理階段。將Mark階段標記出的不可用對象清除,釋放其所佔用的內存空間。主要有如下幾種實現方式。

清除(Sweep)

算法思想:遍歷堆空間,將Mark階段標記不可用的對象清除。

不足: 效率不高;空間問題,屢次清除以後會產生大量的內存碎片。

適用場景:對象壽命長的內存區域。

該算法過程以下圖所示: 
sweep

複製(Copying)

算法思想:將內存劃分爲兩個區域(大小比例可調整),每次只用其中一塊,當此塊內存用完時,就將存活對象複製到另外一塊內存中,並對當前塊進行內存回收。

優勢:解決了內存碎片問題;內存分配效率提升。每次複製後對象在堆中都是線性排列的,所以內存分配時只需移動堆頂指針便可。

不足:若是對象的存活率較高,大量的複製操做會顯著的下降效率;內存空間浪費,每次都只能使用堆空間的一部分,代價高昂。

該算法過程以下圖所示: 
copy

整理(Compacting)

算法思想:將標記的全部可用對象向內存一端移動,而後直接清理邊界之外的內存區域便可。

優勢:相似於複製算法,解決了內存碎片問題,內存分配效率提升;消除了複製算法對內存空間的浪費。

不足:難以作到並行。

該算法過程以下圖所示: 
compact

分代回收(Generational)

前面所述的Mark-Clean算法都是針對整個堆區域的,每一次GC運行都須要對堆中全部的對象進行遍歷。所以,隨着堆中對象數量的增多,GC的效率就會隨之降低。因而,GC對程序運行作出以下假設:

  • 大多數對象都會在建立後不久死亡
  • 若是對象已存活一段時間,那它極可能會繼續存活一段時間

基於這兩個假設,GC將堆中的對象按照存活時間分爲三代:Young(新生代)、Old(老年代)、Perm(永久代)。其內存劃分示意圖以下: 
jvm內存劃分

YOUNG 新生代

由圖可見,新生代又可劃分爲三個區域:Eden,Survivor0,Survivor1。其中,Eden區最大,新對象的內存分配都在此區域進行。兩個Survivor區域一個爲From區,一個爲To區,每次只使用其中的一個。

新生代的垃圾回收採用的是複製算法。第一次GC時,Eden區的存活對象會被複制到S0區。此後每次進行GC時,Eden區和From區的存活對象都會被複制到To區。若是一個對象在經歷了幾回垃圾回收後仍然存活,那麼它就會被複制到Old Generation(老年代),此過程稱爲Promotion。

OLD 老年代

老年代的對象是由新生代對象通過Promotion而來,基於前面列出的假設:「若是對象已存活一段時間,那它極可能會繼續存活一段時間」,該區域的對象存活率廣泛較高,所以通常採用Mark-Sweep或Mark-Compact算法。

PERM 永久代

永久代並不用來存儲從老年代通過Promotion而來的對象,它存儲的是元數據,包括已被虛擬機加載的類信息、常量、靜態變量、方法等。該區域一般不會發生垃圾回收。

安全點/安全區域

在程序執行時,並不是任什麼時候候均可以停下來進行垃圾回收,只有到達某些特定的點時才能暫停,這些點稱爲安全點(Safepoint)。安全點的設定既不能太少以至於讓GC等待時間過長,也不能太頻繁致使運行時負荷增大。通常在方法調用、循環跳轉、異常跳轉處會產生安全點。

那麼,如何在GC發生時讓全部的用戶線程都「跑」到最近的安全點上停下來呢,有如下兩種方案:

一、搶先式中斷:在GC發生時即中斷全部用戶線程,如有的線程中斷的地方不是安全點,則恢復該線程,讓它跑到安全點上再暫停。(幾乎不用) 
二、主動式中斷:GC在其要開始運行前設置一個標誌。而每一個用戶線程在運行過程當中都會去主動的輪詢這個標誌,若是標誌爲真則主動中斷掛起。因爲輪詢標誌的地方和安全點重合,所以線程暫停的地方必定是安全的。

可是,以上實現方案有一種狀況沒法解決,那就是用戶線程不運行的時候,也即處於sleep或blocked狀態的時候。因爲此時線程沒法輪詢中斷標誌,也就不能保證GC開始時它必定處於安全狀態。此時就須要引入安全區域(Safe Region)的概念了,它是指在一段代碼片斷中,對象之間的引用關係不會發生變化。安全區域能夠看作是擴展了的安全點。

當用戶線程執行到Safe Region時,首先會標誌本身進入了安全區域。那麼,就算GC要開始時該線程處於blocked狀態,GC也能夠放心的執行垃圾回收動做了。而當線程要離開Safe Region時,要先檢查GC是否已經完成。若是完成了,線程就能夠繼續執行,不然需等待直到收到能夠安全離開Safe Region的信號爲止。

垃圾收集器

不一樣的虛擬機中一般有不止一種的垃圾收集器,它們實現了不一樣的垃圾收集算法。如下列舉在Sun HotSpot虛擬機中包含的垃圾收集器。

YOUNG 新生代

Serial(-XX:+UseSerialGC)

單線程收集器,採用複製算法。GC運行時會暫停全部的用戶線程(STW,Stop The World)。是虛擬機運行在Client模式下的默認新生代收集器。

ParNew(-XX:+UseParNewGC)

Serial收集器的多線程版本,除此以外與Serial收集器幾乎徹底相同。是許多運行在Server模式下的虛擬機中首選的新生代收集器。緣由之一是它是惟一能與CMS配合使用的新生代收集器。

可以使用-XX:ParallelGCThreads參數指定垃圾收集的線程數。

Parallel Scavenge(-XX:+UseParallelGC)

與ParNew同樣,是使用複製算法的多線程收集器。可是不一樣於ParNew對縮短垃圾收集時用戶線程停頓時間的關注,Parallel Scavenge更多的是關注提升程序的吞吐量,所以常被稱爲「吞吐量優先」收集器。適用於在後臺運算而沒有太多交互的任務。

OLD 老年代

Serial Old

Serial收集器的老年代版本,單線程收集器,使用Mark-Compact算法。

主要用於Client模式下的虛擬機。但在Server模式下也有兩大用途。

  • 在JDK1.5及以前版本中與Parallel Scavenge搭配使用
  • 做爲CMS收集器的後備預案,在發生Concurrent Mode Failure時使用Parallel Old(-XX:+UseParallelOldGC)

Parallel Old(-XX:+UseParallelOldGC)

Parallel Scavenge的老年代版本,使用多線程和Mark-Compact算法,JDK1.6開始提供。

下圖展現了Serial和Parallel收集器的工做模式。 
Serial和Parallel

CMS(-XX:+UseConcMarkSweepGC)

Concurrent Mark Sweep,其工做過程可分爲如下四個步驟:

  • 初始標記(Initial Mark):STW方式。標記GC Roots直接引用的對象,時間很短。
  • 併發標記(Concurrent Mark):進行GC Roots Tracing,標記出全部可用對象。
  • 從新標記(Remark):對併發標記期間因程序繼續運行而變化的引用進行修正,停頓時間比初始標記長,但遠比並發標記短。
  • 併發清除(Concurrent Sweep):清除不可用對象,釋放內存。 
    該過程示意圖以下所示: 
    cms垃圾收集過程

由圖可見,CMS執行過程當中大部分階段都是與用戶線程並行進行的,所以用戶線程暫停時間會大大減小。可是因爲CMS在進行清理時,用戶線程也在運行,也即此時仍然會有新的垃圾產生。這些垃圾稱爲「浮動垃圾」(Floating Garbage)。因爲「浮動垃圾」產生於CMS標記階段以後,它們只能等到下一次GC時纔可被回收。因此,CMS並不能等到老年代幾乎要滿了纔開始垃圾收集動做,它必須預留足夠的空間給用戶線程在垃圾收集過程當中使用。若是預留的空間預估不許的話,就有可能出現如下兩種狀況:

  • Concurrent Mode Failure:在CMS運行期間預留的內存空間不夠用戶線程使用,這將觸發一次Full GC,即啓動後備預案(Serial Old收集器)來從新進行老年代的垃圾收集。這可能會致使數分鐘的用戶線程停頓。
  • Promotion Failure:因爲CMS採用的是Mark-Sweep算法,所以在執行了幾回GC以後老年代會存在大量的內存碎片。若是重新生代通過Promotion而來的對象過大,就頗有可能找不到足夠的空間來分配。這也會提早觸發一次Full GC。

可以使用-XX:+CMSInitiatingOccupancyFraction參數來指定在老年代空間被使用多少後觸發垃圾收集,默認爲68%。

G1(Garbage First,-XX:+UseG1GC)

之因此把G1單獨列出來,是由於它在內存年代劃分上不一樣於上面介紹的全部收集器。G1把內存分爲不少個大小相等的獨立區域(Region),新生代和老年代再也不是相互隔離的,而是都由若干個非連續的Region組成。除此以外,G1收集器還有如下幾個特色:

  • 並行與併發:可大大縮短STW的時間
  • 分代收集:G1能夠不須要其餘收集器的配合而獨立管理整個GC堆,並且它可以使用不一樣的方式去處理不一樣年代的對象
  • 空間整合:G1總體上採用Mark-Compact算法,消除了內存碎片
  • 可預測的停頓:經過跟蹤各個Region中垃圾堆積的價值大小(可回收的空間大小以及回收所需時間的經驗值),G1維護了一個優先列表,每次根據容許的收集時間,優先對價值最大的Region進行回收。 
    固然,因爲跨Region引用的存在,垃圾收集並不能真的以Region爲單位進行。對於這種狀況,G1經過爲每個Region維護一個Remember Set(RSet)來避免進行全堆掃描。RSet中記錄了其餘Region中的對象指向本Region對象的引用信息。

忽略RSet的維護操做,G1的執行過程主要分爲如下四步,其與CMS的執行過程很類似:

一、初始標記(Initial Mark):STW方式。標記GC Roots直接引用的對象,並修改TAMS的值,使下一階段用戶線程併發運行時可以在正確可用的Region中建立新對象。時間很短。 
二、併發標記(Concurrent Mark):進行GC Roots Tracing,標記出全部可用對象。 
三、最終標記(Final Mark):將併發標記期間因程序繼續運行而變化的引用合併到RSet中。 
四、篩選回收(Live Data Counting and Evacuation):對各個Region按照回收價值和成本進行排序,而後根據用戶所指望的GC停頓時間來制定回收計劃。 
其執行過程以下圖所示: 

GC監控

在實際應用中,經常須要根據不一樣的應用特徵調整垃圾收集器的配置方案。在調整過程當中,難免須要監控各類收集器的運行過程來進行性能的比較。JDK自帶了一個Visual VM工具來可視化GC的執行過程。筆者最近爲了跟蹤服務器上不一樣垃圾收集器實現的性能,分析了較多的GC日誌。不一樣收集器生成的日誌格式可能不盡相同,但都有必定的共性。下面列出的是在實際應用中使用ParNew+CMS和使用G1時產生的日誌,從中能夠很清楚的看到CMS和G1的執行階段以及GC運行時用戶線程暫停的時間,有興趣的朋友能夠研究一下
對Java架構技術,對架構技術感興趣的同窗,歡迎加QQqun:859729143 Java點擊進入,一塊兒學習,相互討論。
羣內已經有小夥伴將知識體系整理好(源碼,筆記,PPT,學習視頻),歡迎?加羣免費領取
分享給喜歡Java,喜歡編程,有夢想成爲架構師的程序員們,但願可以幫助到大家。

ParNew+CMS日誌 

G1日誌 

使用的日誌相關參數以下

-verbose:gc 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps 
-XX:+PrintHeapAtGC

其餘注意點

避免顯式調用System.gc()或Runtime.getRuntime().gc()。這兩個方法只是給虛擬機一個建議,是否執行垃圾回收仍是由虛擬機來決定。  不要在finalize()方法中釋放資源。  不要嘗試在finalize()方法中逃脫垃圾回收。

相關文章
相關標籤/搜索