Java虚拟机:垃圾回收机制与垃圾收集器
一、垃圾回收機制:
1、垃圾回收的過程:
JVM內存區域的程序計算器,虛擬機棧、本地方法棧的生命周期是和線程同步的,隨著線程的銷毀而自動釋放內存,所以只有堆和方法區需要GC,方法區主要是針對常量池的回收和對類型的卸載,堆區針對的是不再使用的對象進行回收內存空間。我們常說的GC一般指的是堆區的垃圾回收,堆內存空間可以進一步劃分新生代和老年代,新生代會發生Minor GC,老年代會發生Full GC。
JVM把年輕代分成三部分:一個Eden區和兩個Survivor區(即From區和To區),比例為8:1:1。當Eden區沒有足夠的內存空間給對象分配內存時,虛擬機會發起一次Minor GC,在GC開始的時候,對象會存在Eden和From區,To區是空的。進行GC時,Eden區存活的對象會被復制到To區,From區存活的對象會根據年齡值決定去向,達到閾值(默認15)的對象會被移動到老年代中,沒有達到閾值的對象會被復制到To(但有可能存在沒有達到閾值就從Survivor區直接移動到老年代的情況:在進行GC的時候會對Survivor中的對象進行判斷,Survivor空間中年齡相同的對象的總和大于等于Survivor空間一半的話,年齡大于或等于該年齡的對象就會被復制到老年代;在把Eden區的對象復制到To區的時候,To可能已經滿了,這個時候Eden中的對象就會被直接復制到老年代中)。這時Eden區和From區已經被清空了。接下來From區和To區交換角色,以保證To區在GC開始時是空的。Minor GC會一直重復這樣的過程,直到To區被填滿,To被填滿之后,會將所有對象移動到老年代中。如果老年代內存空間不足,則會觸發一次Full GC。
2、確認對象是否存活:
垃圾收集器在對堆進行回收前,首先要確定對象是否還存活,判斷對象是否存活主要有兩種算法:引用計數算法和可達性分析算法。
(1)引用計數算法:對象創建時,給對象添加一個引用計數器,每當有一個地方引用到它時,計數器值加1;引用失效時,計數器值減1;當計數值值為0時,這個對象就是不可能再被引用的。
(2)可達性分析算法:以“GC Roots”對象為起點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連接時,則證明此對象是不可用的。
GC Roots對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象;
- 本地方法棧中JVM(Native)引用的對象;
- 方法區中類靜態屬性引用的對象;
- 方法區中常量引用的對象。
四種對象引用類別:(關聯強度向下遞減)
- 強引用:GC不會回收強引用的對象。
- 軟引用:系統在發生內存溢出異常之前,會把這些對象列進回收范圍之中,進行第2次回收。(如果內存不緊張,這類對象可以不回收;如果內存緊張,這類對象就會被回收)
- 弱引用:被弱引用關聯的對象,只能生存到下一次垃圾收集之前。
- 虛引用:目的是能在對象被回收時收到一個系統通知。
?3、對象的回收經歷:
目前最普遍使用的判斷對象是否存活的算法是可達性分析算法,對象在真正死亡,需要經歷兩個階段:
(1)可達性分析后,沒有與GC Roots相連接的引用鏈,會被第一次標記并篩選。如果對象沒有覆蓋finalize()方法或已經調用finalize()方法,則不會調用finalize()方法。否則則對象會被放在F-Queue隊列中,等待線程執行finalize()方法。
(2)若對象想要存活下來,finalize()方法是最后的機會,只需在finalize()方法中重新與引用鏈上的對象相關聯,否則,GC對F-Queue隊列進行第二次小規模標記后對象真正死亡。
?4、垃圾收集算法:
確認對象已經不可達之后,在觸發GC時就要對這類對象進行回收,常見的GC算法如下:
(1)標記-清除算法:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。缺點:效率低,會產生大量不連續的內存碎片。
(2)復制算法:將可用內存劃分成大小相等的兩塊,每次只使用其中的一塊,當這塊的內存用完時,就將還存活的對象復制到另一塊內存中,然后再把原來的內存空間清理掉。缺點:內存縮小為原來的一半。
(3)標記-整理算法:首先標記出需要回收的對象,接著將所有存活的對象都向一端移動,然后清理掉端邊界以外的內存。
(4)分代收集算法:根據各個年齡代的特點選擇合適的收集算法。在新生代中,每次垃圾收集都有大量的對象死去,因此采用復制算法。老年代中,因為對象的存活率高,沒有額外的空間對他進行擔保,因此使用“標記-清除”和“標記-整理”算法。
5、對象內存分配策略:
為了避免頻繁發生GC,JVM在為對象分配內存時也定義了一套策略:
(1)對象優先在Eden分配:當Eden沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。
(2)大對象直接進入老年代:避免Eden區及兩個Survivor區之間發生大量的內存復制。
(3)長期存活的對象將進入老年代:對象在Eden區出生,并經過一次Minor GC后仍存活,年齡加1,若年齡超過閾值(默認15),則被晉升到老年代。
(4)動態年齡判斷:Survivor空間中相同年齡所有對象大小大于Survivor空間的一半,年齡大于或等于該年齡的對象直接進入老年代。
(5)空間分配擔保:Minor GC前,虛擬機會檢查老年代最大可用連續空間是否大于新生代所有對象總空間,若成立,則Minor GC是安全的。若不成立,則檢查是否允許擔保失敗,如果允許,檢查老年代最大可用連續空間是否大于歷次晉升到老年代的平均大小,大于,則嘗試進行Minor GC;如果小于或者不允許冒險,則Full GC。
?
二、垃圾收集器:
1、Serial收集器:
Serial收集器是一個新生代收集器,使用復制算法。由于是單線程執行的,所以在進行垃圾收集時,必須暫停其他所有的用戶線程(Stop the world),對于限定單個CPU的環境來說,由于沒有線程切換的開銷,可以獲得最高的單線程收集效率。
是Jvm Client模式下默認的新生代收集器。
2、ParNew收集器:
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其余行為包括Serial收集器完全一樣,包括控制參數、收集算法、Stop The Worl、對象分配規則、回收策略等都。
在多核CPU上,回收效率會高于Serial收集器;反之在單核CPU, 效率會不如Serial收集器。ParNew收集器默認開啟和CPU數目相同的線程數,可以通過-XX:ParallelGCThreads參數來限制垃圾收集器的線程數;
ParNew收集器是許多運行在Server模式下的虛擬機中首選新生代收集器,主要原因是,除Serial收集器之外,目前只有ParNew它能與CMS收集器配合工作。
3、Parallel Scavenge收集器:
Parallel Scavenge收集器是新生代收集器,使用復制算法,并行多線程收集。Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。(吞吐量= 程序運行時間/(程序運行時間 + 垃圾收集時間),虛擬機總共運行了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%)。高吞吐量可以最高效率地利用CPU時間,盡快完成程序的運算任務,主要適用于在后臺不需要太多交互的任務。
Parallel Scavenge收集器提供了兩個參數用于精準控制吞吐量:
(1)-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,是一個大于0的毫秒數。
(2)-XX:GCTimeRation:直接設置吞吐量大小,是一個大于0小于100的整數,也就是程序運行時間占總時間的比率,默認值是99,即垃圾收集運行最大1%(1/(1+99))的垃圾收集時間。
(3)支持自適應的GC調節策略。它還提供一個參數:-XX:+UseAdaptiveSizePolicy,這是個開關參數,打開之后就不需要手動指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、新生代晉升年老代對象年齡(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據當前系統運行情況收集性能監控信息,動態調整這些參數以達到最大吞吐量。
4、Serial Old收集器:
Serial Old是Serial收集器的老年代版本,使用單線程執行和“標記-整理”算法。
主要用途:client模式下默認的老年代垃圾收集器。在server模式下主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的后備垃圾收集方案,在并發收集發生 Concurrent Mode Failure的時候,臨時啟動Serial Old收集器重新進行老年代的垃圾收集。
5、Parallel Old收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,jdk1.6之后開始提供,使用多線程和“標記-整理”算法。
在JDK1.6之前,新生代使用Parallel Scavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。
6、CMS收集器:
CMS(Concurrent Mark Sweep)收集器應用于老年代,采用多線程和“標記-清除”算法實現的,實現真正意義上的并發垃圾收集器,是一種以獲取最短回收停頓時間為目標的收集器。整個收集過程大致分為4個步驟,如下圖所示:
- (1)初始標記(CMS initial mark):需要停頓所有用戶線程,初始標記僅僅是標記出GC ROOTS能直接關聯到的對象,速度很快。
- (2)并發標記(CMS concurrent?mark):進行GC ROOTS 根搜索算法階段,會判定對象是否存活,和用戶線程一起工作,不需要暫停工作線程。
- (3)重新標記(CMS remark):為了修正并發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。需要停頓所有用戶線程,停頓時間會被初始標記階段稍長,但比并發標記階段要短。
- (4)并發清除(CMS concurrent sweep):清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。
? ? ?整個過程中耗時最長的并發標記和并發清除過程中,收集器線程都可以與用戶線程一起工作,所以整體來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。
CMS收集器的雖然真正意義上實現了并發收集以及低停頓,但CMS還遠遠達不到完美,主要有四個顯著缺點:
(1)CMS收集器對CPU資源非常敏感。在并發階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致引用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是:(CPU數量+3) / 4。
(2)CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗后而導致另一次Full ?GC的產生。由于CMS并發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱為“浮動垃圾”。
(3)由于在垃圾收集階段用戶線程還需要運行,即需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分內存空間提供并發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以降低內存回收次數提高性能。要是CMS運行期間預留的內存無法滿足程序其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置的過高將會很容易導致“Concurrent Mode Failure”失敗,性能反而降低。
(4)CMS是基于“標記-清除”算法實現的收集器,會產生大量不連續的內存碎片。空間碎片太多時,如果無法找到一塊足夠大的連續內存存放對象時,將不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用于在Full ?GC之后增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full ?GC之后,跟著來一次碎片整理過程。
7、G1收集器:
(1)G1(Garbage First)收集器是JDK1.7提供的一個新收集器,與CMS收集器相比,最突出的改進是:
- 基于“標記-整理”算法實現,不會產生內存碎片。
- 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
其他特點:
- 并行性: 回收期間, 可由多個線程同時工作, 有效利用多核cpu資源;
- 并發性: 與應用程序可交替執行, 部分工作可以和應用程序同時執行,
- 分代GC: 分代收集器,同時兼顧年輕代和老年代。他能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過了多次GC的對象,以便獲取更好的GC效果。
(2)垃圾收集原理:G1收集器并不采用新生代和老年代物理隔離的傳統布局方式(僅在邏輯上劃分新生代和老年代),而是將整個堆內存劃分為2048個大小相等(具體大小根據堆的實際大小而定)的獨立內存塊Region,每個Region是邏輯連續的一段內存,整體被控制在1M、2M、4M、8M、16M和32M,總之是2的冪次方。G1收集器跟蹤Region中的垃圾堆積情況,并在后臺維護一個優先級列表,每次根據設置的垃圾回收時間,回收優先級最高的區域,這樣可以避免整個新生代或整個老年代的垃圾回收,使得stop the world的時間更短、更可控,同時在有限的時間內可以獲得最高的回收效率。區域劃分和優先級區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。
(3)G1的收集過程:
如果不考慮維護Remembered Set的操作,可以分為上圖4個步驟(與CMS較為相似),其中初始標記、并發標記、最終標記跟CMS收集器相同,只有第四階段的篩選回收有些區別。
篩選回收:首先排序各個Region的回收價值和成本,然后根據用戶期望的GC停頓時間來制定回收計劃,?最后按計劃回收一些價值高的Region中垃圾對象。
?
附:JVM的新生代除了Eden區,為什么還設置兩個Survivor區?
該部分轉自:https://blog.csdn.net/antony9118/article/details/51425581#commentBox
1、為什么要有Survivor區:
先不去想為什么有兩個Survivor區,第一個問題是,設置Survivor區的意義在哪里?
如果沒有Survivor,Eden區每進行一次Minor GC,存活的對象就會被送到老年代,老年代很快被填滿,觸發Full GC。老年代的內存空間遠大于新生代,進行一次Full GC消耗的時間比Minor GC長得多。頻發的Full GC消耗的時間是非常可觀的,這一點會影響大型程序的執行和響應速度,更不要說某些連接會因為超時發生連接錯誤了。
那在沒有Survivor的情況下,有沒有什么解決方案可以避免上述情況:
| 增加老年代空間 | 更多存活對象才能填滿老年代。降低Full GC頻率 | 隨著老年代空間加大,一旦發生Full GC,執行所需要的時間更長 |
| 減少老年代空間 | Full GC所需時間減少 | 老年代很快被存活對象填滿,Full GC頻率增加 |
顯而易見,沒有Survivor的話,上述兩種解決方案都不能從根本上解決問題。
我們可以得到第一條結論:Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的對象,才會被送到老年代。
2、為什么要設置兩個Survivor區:
設置兩個Survivor區最大的好處就是解決了碎片化,下面我們來分析一下。
為什么一個Survivor區不行?第一部分中,我們知道了必須設置Survivor區。假設現在只有一個survivor區,我們來模擬一下流程:?
剛剛新建的對象在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活對象就會被移動到Survivor區。這樣繼續循環下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活對象,如果此時把Eden區的存活對象硬放到Survivor區,很明顯這兩部分對象所占有的內存是不連續的,也就導致了內存碎片化。?
我繪制了一幅圖來表明這個過程。其中色塊代表對象,白色框分別代表Eden區(大)和Survivor區(小)。
碎片化帶來的風險是極大的,嚴重影響JAVA程序的性能。堆空間被散布的對象占據不連續的內存,最直接的結果就是,堆中沒有足夠大的連續內存空間,接下去如果程序需要給一個內存需求很大的對象分配內存,就會由于內存不足觸發Minor GC了。
那么如果建立兩塊Survivor區呢?剛剛新建的對象在Eden中,經歷一次Minor GC,Eden中的存活對象就會被移動到第一塊From survivor區,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和From區中的存活對象又會被復制送入第二塊To survivor區中(這個復制算法保證了To區中來自From和Eden兩部分的存活對象占用連續的內存空間,避免了碎片化的發生)。From和Eden被清空,然后下一輪From?survivor與To survivor交換角色,如此循環往復。如果對象的復制次數達到16次,該對象就會被送到老年代中。
?
上述機制最大的好處就是,整個過程中,永遠有一個survivor是空的,另一個非空的survivor無碎片。
那么,Survivor為什么不分更多塊呢?比方說分成三個、四個、五個?顯然,如果Survivor區再細分下去,每一塊的空間就會比較小,很容易導致Survivor區滿,因此,我認為兩塊Survivor區是經過權衡之后的最佳方案。
總結
以上是生活随笔為你收集整理的Java虚拟机:垃圾回收机制与垃圾收集器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringCloud OpenFeig
- 下一篇: Java虚拟机:对象创建过程与类加载机制