七种垃圾收集器和垃圾回收、分代收集、GCROOTS相关概念、GC如何判断一个对象可以被回收
文章目錄
- 垃圾收集器概述
- 垃圾回收算法
- 1)標記—清除算法(Mark-Sweep)(DVM 使用的算法)
- 2)復制算法(Copying)
- 3)標記—整理算法(Mark-Compact)
- 4)分代收集(Generational Collection)
- 什么是Stop The World ?
- GC如何判斷一個對象可以被回收
- 垃圾回收器是怎樣尋找 GC Roots 的?
- 如何判斷一個常量是廢棄常量
- 新生代 年老代
- JVM的永久代中會發生垃圾回收么?
- 1.Serial
- 2.Serial Old 收集器
- 3.ParNew
- 4.Parallel Scavenge
- 5.Parallel Old
- 6.CMS收集器?
- 7.G1收集器?
- G1與CMS兩個垃圾收集器的對比
- 如何選擇垃圾收集器?
垃圾收集器概述
新生代收集器(全都是復制算法):serial、parnew、parallel scavenge
老生代收集器:cms(標記-清除算法)、serial old(標記-整理)、paralle old(標記整理)
整堆收集器:g1(一個region中是標記-清除算法,二個region是復制算法)
paralle:并行,多個垃圾收集器并行工作,此時用戶線程處于等待狀態
并發(concurrent):用戶線程和垃圾收集器同時執行
吞吐量:運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間)
JDK誕生之后第一個垃圾回收器就是Serial,和Serial Old。追隨提高效率,誕生了PS,為了配合CMS.誕生了PN.CMS是1.4版本后期引入,CMS是里程碑式的GC,它開啟了并發回收的過程,但是CMS毛病較多,因此目前沒有任何一個JDK版本默認是CMS。所謂的Serial指的是單線程。Parallel Scavenge指的是多線程。常見的垃圾回收器組合最常用的是有三種(Serial+Serial Old)、(Parallel Scavenge+ParallelOld)、(ParNew+CMS)。
看圖中畫的紅線,但凡是能連接在一起的都可以組合。前面幾種不僅都是在邏輯上分年輕代和老年代,在物理上也是分年輕代和老年代的。G1只是在邏輯上分年輕代老年代,在物理上他就分成一塊一塊的了,另外需要注意的是CMS還有一個組合是和Serial Old組合到一起的。
下面解釋一些垃圾收集中相關的概念
垃圾回收算法
1)標記—清除算法(Mark-Sweep)(DVM 使用的算法)
標記—清除算法包括兩個階段:“標記”和“清除”。在標記階段,確定所有要回收的對象,并做標記。清除階段緊隨標記階段,將標記階段確定不可用的對象清除。標記—清除算法是基礎的收集算法 后續的所有算法都是對其的不足進行改進得到的,標記和清除階段的效率不高,而且清除后會產生大量的不連續空間,這樣當程序需要分配大內存對象時,可能無法找到足夠的連續空間。
2)復制算法(Copying)
復制算法是把內存分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的對象復制到另一塊上,然后把這塊內存整個清理掉。復制算法實現簡單,運行效率高,但是由于每次只能使用其中的一半,造成內存的利用率不高。現在的JVM用復制方法收集新生代,由于新生代中大部分對象(98%)都是朝生夕死的,所以兩塊內存的比例不是1:1(大概是8:1)。
3)標記—整理算法(Mark-Compact)
標記—整理算法和標記—清除算法一樣,但是標記—整理算法不是把存活對象復制到另一塊內存,而是把存活對象往內存的一端移動,然后直接回收邊界以外的內存。標記—整理算法提高了內存的利用率,并且它適合在收集對象存活時間較長的老年代。
4)分代收集(Generational Collection)
分代收集是根據對象的存活時間把內存分為新生代和老年代,根據各個代對象的存活特點,每個代采用不同的垃圾回收算法。新生代采用復制算法,老年代采用標記—整理算法。垃圾算法的實現涉及大量的程序細節,而且不同的虛擬機平臺實現的方法也各不相同。
當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊,一般java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法
什么是Stop The World ?
進行垃圾收集時,必須暫停其他所有工作線程,Sun將這種事情叫做"Stop The World”
進行垃圾回收的過程中,會涉及對象的移動。為了保證對象引用更新的正確性,必須暫停所有的用 戶線程,像這樣的停頓,虛擬機設計者形象描述為「Stop The World」。也簡稱為STW。
為什么需要 STW?
在 java 應用程序中「「引用關系」」是不斷發生「「變化」」的,那么就會有會有很多種情況來導致「「垃圾標識」」出錯。想想一下如果 Object a 目前是個垃圾,GC 把它標記為垃圾,但是在清除前又有其他對象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果沒有 STW 就要去無限維護這種關系來去采集正確的信息。再舉個例子,到了秋天,道路上灑滿了金色的落葉,環衛工人在打掃街道,卻永遠也無法打掃干凈,因為總會有不斷的落葉。
垃圾標記階段?
在GC執行垃圾回收之前,為了區分對象存活與否,當對象被標記為死亡時,GC才回執行垃圾回收,這 個過程就是垃圾標記階段。
GC如何判斷一個對象可以被回收
引用計數法(已被淘汰的算法)
1.每一個對象有一個引用屬性,新增一個引用時加一,引用釋放時減一,計數為0的時候可以回收。但是這種計算方法,有一個致命的問題,無法解決循環引用的問題
可達性分析算法(根引用)
1.從GcRoot開始向下搜索,搜索所走過的路徑被稱為引用鏈,當一個對象到GcRoot沒有任何引用鏈相連時,則證明此對象是不可用的,那么虛擬機就可以判定回收。
2.那么GcRoot有哪些?
1.虛擬機棧中引用的對象
2.方法區中靜態屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧中(即一般說的native方法)引用的對象不同的引用類型
不同的引用類型的回收機制是不一樣的
1.強引用:通過關鍵字new的對象就是強引用對象,強引用指向的對象任何時候都不會被回收,寧愿OOM也不會回收
2.軟引用:如果一個對象持有軟引用,那么當JVM堆空間不足時,會被回收。如果內存空間足夠則不會被回收。一個類的軟引用可以通過iava.lang.ref.SoftReference持有
3.弱引用:如果一個對象持有弱引用,那么在G時,只要發現弱引用對象,就會被回收。一個類的弱引用可以通過java.lang.ref.WeakReference持有。弱引用與軟引用的區別:弱引用擁有更短的生命周期,在垃圾回收器線程掃描它所管轄的內存區域中,一旦發現了只具有弱引用的對象,不管當前內存空間是否足夠都會回收它的內存,不過由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象
4.虛引用:幾乎和沒有一樣,隨時可以被回收。通過PhantomReference持有 。虛引用主要用來跟蹤對象被垃圾回收的活動。在程序設計中一般很少用弱引用與虛引用,軟引用的情況較多,因為軟引用可以加速jvm內存回收速度,可以維護系統的運行安全,防止內存溢出(OOM)問題的產生
垃圾回收器是怎樣尋找 GC Roots 的?
我們在前面說明了根可達算法是通過 GC Roots 來找到存活的對象的,也定義了 GC Roots,那么垃圾回收器是怎樣尋找GC Roots 的呢?首先,「「為了保證結果的準確性,GC Roots枚舉時是要在STW的情況下進行的」」,但是由于 JAVA 應用越來越大,所以也不能逐個檢查每個對象是否為 GC Root,那將消耗大量的時間。一個很自然的想法是,能不能用空間換時間,在某個時候把棧上代表引用的位置全部記錄下來,這樣到真正 GC 的時候就可以直接讀取,而不用再一點一點的掃描了。事實上,大部分主流的虛擬機也正是這么做的,比如 HotSpot ,它使用一種叫做 「「OopMap」」 的數據結構來記錄這類信息。
如何判斷一個常量是廢棄常量
運行時常量池主要回收的是廢棄的常量,假如在常量池中存在字符串“abc"如果當前沒有任何string對象引用該字符串,說明”abc"是廢棄常量,這時如果發生內存回收的話而且有必要的話“abc"就會被系統清理出常量池
新生代 年老代
由于現在的垃圾收集器都采用分代收集算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為 Eden、Survivor(又可分為From Survivor 和 To Survivor)、Tenured
1.為什么會有年輕代
我們先來縷一縷,為什么需要把堆分代?不分代不能完成他所做的事情么?其實不分代完全可以,分代的唯一理由就是優化GC性能。你先想想,如果沒有分代,那我們所有的對象都在一塊,GC的時候我們要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而我們的很多對象都是朝生夕死的,如果分代的話,我們把新創建的對象放到某一地方,當GC的時候先把這塊存“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。
2.年輕代中的GC
HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例為8:1,為啥默認會是這個比例,接下來我們會聊到。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC后,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。
因為年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是復制算法,復制算法的基本思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活著的對象復制到另外一塊上面。復制算法不會產生內存碎片。
在GC開始的時候,對象只會存在于Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的對象都會被復制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被復制到“To”區域。經過這次GC后,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,“To”區被填滿之后,會將所有對象移動到年老代中。
3.一個對象的這一輩子
我是一個普通的java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認識了很多人。在年老代里,我生活了20年(每次GC加一歲),然后被回收。
4.有關年輕代的JVM參數
1)-XX:NewSize和-XX:MaxNewSize
用于設置年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。
2)-XX:SurvivorRatio
用于設置Eden和其中一個Survivor的比值,這個值也比較重要。
3)-XX:+PrintTenuringDistribution
這個參數用于顯示每次Minor GC時Survivor區中各個年齡段的對象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于設置晉升到老年代的對象年齡的最小值和最大值,每個對象在堅持過一次Minor GC之后,年齡就加1。
年老代(Tenured Gen):年老代主要存放JVM認為生命周期比較長的對象(經過幾次的Young Gen的垃圾回收后仍然存在),內存大小相對會比較大,垃圾回收也相對沒有那么頻繁
JVM的永久代中會發生垃圾回收么?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是為什么正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元數據區 (注:Java8 中已經移除了永久代,新加了一個叫做元數據區的native內存區)
1.Serial
Serial是一個「「單線程」」的垃圾回收器,「「采用復制算法負責新生代」」的垃圾回收工作,可以與 CMS 垃圾回收器一起搭配工作。,單線程的含義在于它會stop the world。垃圾回收時需要stop the world,直到它收集結 束。所以這種收集器體驗比較差。
在 STW 的時候「「只會有一條線程」」去進行垃圾收集的工作,所以可想而知,它的效率會比較慢。但是他確是所有垃圾回收器里面消耗額外內存最小的,沒錯,就是因為簡單。
這個收集器新生代用復制算法,老年代用標記整理算法。
2.Serial Old 收集器
老年代單線程收集器,Serial收集器的老年代版本;Serial Old 收集器是在 TenuredGeneration 老年代上實現收集的,Serial Old 收集器 所使用的垃圾回收算法是標記-壓縮-清理算法。在回收階段,將標記對象越過堆的空閑區移動到堆的另一端,所有被移動的對象的引用也會被更新指向新的位置。
3.ParNew
ParNew 是一個「「多線程」」的垃圾回收器,「采用復制算法負責新生代」的垃圾回收工作,可以與CMS垃圾回收器一起搭配工作。
它其實就是 Serial 的多線程版本,主要區別就是在 STW 的時候可以用多個線程去清理垃圾。除了使用采用并行收回的方式回收內存外,其他行為幾乎和Serial沒區別。
可以通過選項“-XX:+UseParNewGC”手動指定使用ParNew收集器執行內存回收任務。
4.Parallel Scavenge
Parallel Scavenge垃圾收集器因為與吞吐量關系密切,也稱為吞吐量收集器(Throughput Collector),是一個新生代收集器,也是復制算法的收集器,同時也是多線程并行收集器,與PartNew不同是,它重 點關注的是程序達到一個可控制的吞吐量(Thoughput, CPU用于運行用戶代碼的時間/CPU總消耗時 間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU時間,盡快地完成程序的運算任務,主要適用于在后臺運算而不需要太多交互的任務。
他可以通過2個參數精確的控制吞吐量,更高效的利用cpu。
分別是:-XX:MaxCcPauseMillis 和-XX:GCTimeRatio
新生代用復制算法,老年代用標記整理算法。
5.Parallel Old
Parallel Old 收集器:Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。JDK 1.6中才開始提供。
ParNew,其實就是Parallel New的意思,和Parallel Scavenge沒什么區別,就是它的新版本做了
一些增強以便能讓它和CMS配合使用,CMS某一個特定階段的時候ParNew會同時運行。所以這個才是第三個誕生的。 我工作的時候,其余線程不能工作,必須等GC回收器結束才可以
6.CMS收集器?
CMS非常重要,因為它誕生了一個里程碑式的東西。原來所有的垃圾回收就是在我干活兒的時候你其它的不能干活兒。我垃圾回收器來了,其它所有工作的線程都得給我停等著我回收,我走了你才能繼續工作。CMS的誕生就消除了這種疑問。CMS毛病非常多。以至于目前任何jdk版本默認都不是CMS。
Concurrent Mar Sweep收集器是一種以獲取最短回收停頓時間為目標的收集器。重視服務的響應速 度,希望系統停頓時間最短。采用標記-清除的算法來進行垃圾回收。
CMS垃圾回收的步驟?
初始標記(stop the world)
并發標記
重新標記(stop the world)
并發清除
第一個階段叫做CMS initial mark(初始標記階段)。很簡單,就是我直接找到最根上的對象,其他的對象我不標記,直接標記最根上的
第二個是CMS concurrent mark(并發標記),據統計百分之八十的GC的時間是浪費在這里,因此它把這塊最浪費時間的和我們的應用程序同時運行,對客戶來說感覺可能是慢了一些,但至少你還有反應。并發標記。就是你一邊產生垃圾,我這一邊跟著標記但是這個過程是很難完成的。
所以最后又有一個CMS remark(重新標記)。這又是一個STW。在并發標記過程中產生的那些新的垃圾
在重新標記里頭給它標記一下,這個時候需要你們倆停一下,時間不長。
最后是一個concurrent sweep(并發清理)的過程。并發清理也有它的問題,并發清理過程也會產生新的垃圾,這個時候的垃圾叫做浮動垃圾,浮動垃圾就得等著下一次CMS再一次運行的過程把它給清理掉。
初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。
并發標記就是進行Gc Roots Tracing的過程。
重新標記則是為了修正并發標記期間,因用戶程序繼續運行而導致的標記產生變動的那一部分對象的 標記記錄,這個階段停頓時間一般比初始標記時間長,但是遠比并發標記時間短。
整個過程中并發標記時間最長,但此時可以和用戶線程一起工作。
CMS收集器優點?缺點?
優點:
并發收集,低停頓
理由:由于在整個過程和中最耗時的并發標記和 并發清除過程收集器程序都可以和用戶線程一起工作,所以總體來說,Cms收集器的內存回收過程是與用戶線程一起并發執行的。
缺點:
1、CMS收集器對CPU資源非常敏感
在并發階段,雖然不會導致用戶線程停頓,但是會因為占用了一部分線程使應用程序變慢,總吞吐量會降低,為了解決這種情況,虛擬機提供了一種“增量式并發收集器” 的CMS收集器變種, 就是在并發標記和并發清除的時候讓GC線程和用戶線程交替運行,盡量減少GC 線程獨占資源的時間,這樣整個垃圾收集的過程會變長,但是對用戶程序的影響會減少。(效果不明顯,不推薦)
2、 CMS處理器無法處理浮動垃圾
CMS在并發清理階段線程還在運行, 伴隨著程序的運行自然也會產生新的垃圾,這一部分垃圾產生在標記過程之后,CMS無法在當次過程中處理,所以只有等到下次gc時候在清理掉,這一部分垃圾就稱作“浮動垃圾” 。
3、CMS是基于“標記–清除”算法實現的,所以在收集結束的時候會有大量的空間碎片產生。
空間碎片太多的時候,將會給大對象的分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩余,但是無法找到足夠大的連續空間來分配當前對象的,只能提前觸發 full gc。
為了解決這個問題,CMS提供了一個開關參數,用于在CMS頂不住要進行full gc的時候開啟內存碎片的合并整理過程,內存整理的過程是無法并發的,空間碎片沒有了,但是停頓的時間變長了。
從線程的角度理解 ,它垃圾回收的線程和工作線程同時進行,叫做concurrent mark sweep
(concurrent 并發)。不管你用幾個線程進行垃圾回收這個過程都太長了。在內存比較小的情況下,沒有問題,速度很快。但是現在的服務器內存越來越大,大到什么程度,原來是一個房間,現在可以看成一個天安門廣場。作為一個這么大的內存無論你多少個線程來清理一遍也得需要特別長的時間。以前大概在有10G內存的時候他用PS+PO停頓時間清理一次,大概需要11秒鐘。 有人用CMS,這個最后也會產生碎片之后產生FGC,FGC默認的STW最長的到10幾個小時,就直接卡死在哪兒了,大家什么都干不了。
什么條件觸發CMS呢?老年代分配不下了,處理不下會觸動CMS。初始標記是單線程,重新標記是多線程。
CMS的缺點:cms出現問題會調用Serial Old老年代出來使用單個線程進行標記壓縮
7.G1收集器?
Garbage First收集器是當前收集器技術發展的最前沿成果。jdk 1.6_update14中提供了 g1收集器。
G1收集器是基于標記-整理算法的收集器,它避免了內存碎片的問題。
可以非常精確控制停頓時間,既能讓使用者明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集 上的時間不多超過N毫秒,這幾乎已經是實時java(rtsj )的垃圾收集器特征了。
G1收集器是如何改進收集方式的?
極力避免全區域垃圾收集,之前的收集器進行收集的范圍都是整個新生代或者老年代。而g1將整個Java 堆(包括新生代、老年代)劃分為多個大小固定的獨立區域,并且跟蹤這些區域垃圾堆積程度,維護一 個優先級,每次根據允許的收集時間,優先回收垃圾最多的區域。從而獲得更高的效率。
G1與CMS兩個垃圾收集器的對比
細節方面不同
1.G1在壓縮空間方面有優勢。
2.G1通過將內存空間分成區域( Region)的方式避免內存碎片問題。
3.Eden, Survivor,Old區不再固定、在內存使用效率上來說更靈活。
4.G1可以通過設置預期停頓時間( Pause Time)來控制垃圾收集時間避免應用雪崩現象。
5.G1在回收內存后會馬上同時做合并空閑內存的工作、而CMS默認是在STW(stopthe world)的時候做。
6.G1會在 Young GC中使用、而CMS是老年代并發垃圾收集器
吞吐量優先:G1
響應優先:CMS
CMS的缺點是對cpu的要求比較高。G1是將內存化成了多塊,所有對內段的大小有很大的要求
CMS是清除,所以會存在很多的內存碎片。G1是整理,所以碎片空間較小。
如何選擇垃圾收集器?
參數: -XX:+UseSerialGC 。
參數: -XX:+UseSerialGC 。
參數: -XX:+UseParallelGC 。
4.如果你的應用對響應時間要求較高,想要較少的停頓。甚至 1 秒的停頓都會引起大量的請求失敗,那么選擇G1 、ZGC 、CMS 都是合理的。雖然這些收集器的 GC 停頓通常都比較短,但它需要一些額外的資源去處理這些工作,通常吞吐量會低一些。
參數:
-XX:+UseConcMarkSweepGC 、
-XX:+UseG1GC 、
-XX:+UseZGC 等。
總結
以上是生活随笔為你收集整理的七种垃圾收集器和垃圾回收、分代收集、GCROOTS相关概念、GC如何判断一个对象可以被回收的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot+vue用webso
- 下一篇: 离散数学复习命题公式的范式