最近線上出現了JVM 頻繁FGC的問題,查詢了不少GC相關的資料,作了一些整理翻譯。文章比較長能夠收藏後慢慢閱讀。html
一個垃圾回收器有一下三個職責java
這裏提到的有引用
是指存活的對象,後面會提到一些算法用來判斷對象是否存活。不在有引用的對象將被認爲是死亡的,也就是常說的垃圾garbage
。找到並釋放這些垃圾對象佔用的空間的過程就被稱做是垃圾回收garbage collection
。算法
垃圾回收能夠解決不少內存分配的問題,但並不意味這所有。 好比:你能夠不斷地建立對象並保持對它們的引用直到沒有可用的內存分配。垃圾回收自己就是一項很是複雜和消耗資源的過程。安全
在設計一款垃圾收集器時,有一些選擇可供選擇:bash
串行收集,即便在多cpu環境中也是單線程處理垃圾收集工做。當使用並行收集時,垃圾收集任務就會被分爲几子任務由不一樣的線程的執行,不只僅是在多CPU環境中使用,在單核的系統中也可使用,只是收集效果可能比使用串行效率還低。因此再單核的環境下儘可能使用串行收集。服務器
併發是指垃圾收集線程和應用線程同時執行,併發和stop-the-word
並非互斥的,在一個執行一次垃圾收集的過程當中兩種狀況均可能存在。例如CMS
、G1
垃圾蒐集器。併發式GC會併發執行其垃圾收集任務,可是,可能也會有一些步驟須要以stop-the-world
方法執行,致使應用程序暫停。與併發式GC
相比,Stop-the-world
式的GC更簡單.多線程
這個描述的主要是垃圾被收集之後,對內存碎片的處理方式。併發
整理、不整理,垃圾回收之後是否將存活的對象統一移動到一個地方。整理後的內存空間方便後續的對象分配內存,可是更消耗資源和時間,而不整理效率更高存在內存碎片的風險。oracle
複製,首先將內存分割成兩塊同樣大小的區域,垃圾收集後會將存活的對象拷貝到另外一塊不一樣的內存區域。這樣作的好處是,拷貝後,源內存區域能夠做爲一塊空的、當即可用的區域對待,方便後續的內存分配,可是這種方法的缺點是須要用額外的時間、空間來拷貝對象。jvm
Jvm
要對回收一個對象必須知道這個對象是否存活,便是否有有效的引用?介紹幾種判斷對象是否死亡的算法。
引用計數法 給對象添加一個引用計數器,每次引用到它時引用計數器加一,當引用失效時引用計時器減一。當引用計數器爲0時即表示當前對象能夠被回收。 這個算法實現簡單、斷定效率也很高,可是沒法處理循環引用的問題,即 A 對象引用了 B, B 對象也引用了 A,那麼A、B都有引用,他們的應用計數都爲一,但實際他們是能夠被回收的。
可達性分析算法 算法規定了一些稱爲GC Root
的根對象,當對象沒有引用鏈到達這些GC Root
時就被斷定爲可回收的對象。
當使用稱爲分代收集的技術時,內存將被分爲不一樣的幾代,即,會將對象按其年齡分別存儲在不一樣的對象池中。例如,目前最普遍使用的是分代是將對象分爲年輕代對象和老年代對象。
在分代內存管理中,使用不一樣算法對不一樣代的對象執行垃圾收集的工做,每種算法都是基於對某代對象的特性進行優化的。考慮到應用程序能夠是用包括Java在內的不一樣的程序語言編寫,分代垃圾收集使用了稱爲 弱代理論(weak generational hypothesis)的方法,具體描述以下:
大多數分配了內存的對象並不會存活太長時間,在處於年輕代時就會死掉; 不多有對象會從老年代變成年輕代。 年輕代對象的垃圾收集相對頻繁一些,同時會也更有效率,更快一些,由於年輕代對象所佔用的內存一般較小,也比較容易肯定哪些對象是已經沒法再被引用的。
當某些對象通過幾回年輕代垃圾收集後依然存活,則這些對象會被 提高(promoted)到老年代。典型狀況下,老年代所佔用的內存會比年輕代大,並且還會隨時漸漸慢慢增大。這樣的結果是,對老年代的垃圾收集就不能頻繁進行,並且執行時間也會長不少。
選擇年輕代的垃圾收集算法時會更看重執行速度,由於年輕代的垃圾收集工做會頻繁執行。另外一方面,管理老年代的算法則更注重空間效率,由於老年代會佔用堆中的大部分空間,這要求算法必需要處理好垃圾收集的工做,儘可能下降堆中的垃圾內存的密度。
主要介紹幾種常見的垃圾收集器串行收集器(Serial Collector)
、並行垃圾收集器(Parallel Collector)
、並行整理收集器(Parallel Compacting Collector)
、併發標記清理垃圾收集器(Concurrent Mark-Sweep,CMS)
、Garbage-First (G1)
圖中有連線的表示能夠組合使用。
在Java HotSpot虛擬機中,內存被分爲3代:年輕代、老年代和永生代(java8已經取消永久代)。大多數對象最初都是分配在年輕代內存中的,年輕代中對象通過幾回垃圾收集後還存活的,會被轉到老年代。一些體積比較大的對象在建立的時候可能就會在老年代中。 在年輕代中包含三個分區,一個 Eden區和兩個 Survivor區(FROM、TO),如圖所示。大部分對象最初是分配在Eden區中的(可是,如前面所述,一些較大的對象可能會直接分配在老年代中)。Survivor始終保持一個區域爲空,當通過必定次數(-XX:MaxTenuringThreshold=n
來指定默認值爲15)的年輕代GC後依然存活的對象能夠被晉升到老年代。
當年輕代被填滿時,開始執行年輕代的垃圾收集(minor collection
)。當老年代被填滿時,也會執行老年代垃圾收集(full GC
,major collection
),通常來講,年輕代GC會先執行,執行屢次young GC 會觸發FGC
,固然這不是絕對的,由於大對象會直接分配到老年代,當老年代的分配的內存不足時就可能觸發頻繁的FGC
。目前除了CMS
收集器外,在執行FGC
的時候都會對整個堆進行垃圾收集。
使用串行收集器,年輕代和老年代的垃圾收集工做會串行完成(在單一CPU系統上),這時是stop-the-world模式的。即,當執行垃圾收集工做時,應用程序必須中止運行。
圖3展現了使用串行收集器的年輕代垃圾收集的執行過程。Eden
和Survivor FROM
區存活的對象會被拷貝到初始爲空的另外一個Survivor區(圖中標識爲To的區)中,這其中,那些體積過大以致於Survivor
區裝不下的對象會被直接拷貝到老年代中。相對於已經被拷貝到To區的對象,源Survivor
區(圖中標識爲From的區)中的存活對象仍然比較年輕,而被拷貝到老年代中對象則相對年紀大一些。
在年輕代垃圾收集完成後,Eden區和From區會被清空,只有To區會繼續持有存活的對象。此時,From區和To區在邏輯上交換,To區變成From區,原From區變成To區,如圖4所示。
對於串行收集器,老年代和永生代會在進行垃圾收集時使用標記-清理-整理(Mark-Sweep-Compact)算法。在標記階段,收集器會標識哪些對象是live狀態的。清理階段會跨代清理,標識垃圾對象。而後,收集器執行整理(sliding compaction),將存活對象移動到老年代內存空間的起始部分(永生代中狀況於此相似),這樣在老年代內存空間的尾部會產生一個大的連續空間。如圖5所示。這種整理可使用碰撞指針完成。
大多數運行在客戶機上的應用程序會選擇使用並行垃圾收集器,由於這些應用程序對低暫停時間並無較高的要求。對於當今的硬件來講,串行垃圾收集器已經能夠有效的管理許多具備64M堆的重要應用程序,而且執行一次完整垃圾收集也不會超過半秒鐘。
在J2SE 5.0的發行版中,在非服務器類使用的機器上,默認選擇的是串行垃圾收集器。在其餘類型使用的機器上,能夠經過添加參數 -XX:+UseSerialGC來顯式的使用串行垃圾收集器。
當前,不少的Java應用程序都跑在具備較大物理內存和多CPU的機器上。並行垃圾收集器,也稱爲吞吐量垃圾收集器,被用於垃圾收集工做。該收集器能夠充分的利用多CPU的特色,避免一個CPU執行垃圾收集,其餘CPU空閒的狀態發生。
這裏,對年輕代的並行垃圾收集使用的串行垃圾收集算法的並行版本。它仍然會stop-the-world,拷貝對象,但執行垃圾收集時是使用多CPU並行進行的,減小了垃圾收集的時間損耗,提升了應用程序的吞吐量。圖6展現了串行垃圾收集器和並行垃圾收集器對年輕代進行垃圾收集時的區別。
老年代中的並行垃圾收集使用了與串行垃圾收集器相同的串行 標記-清理-整理(mark-sweep-compact)算法。
當應用程序運行在具備多個CPU上,對暫停時間沒有特別高的要求時,使用並行垃圾收集器會有較好的效果,由於雖不頻繁,但可能時間會很長的老年代垃圾收集仍然會發生。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序更適合使用並行垃圾收集。
可能你會想用並行整理垃圾收集器(會在下一節介紹)來替代並行收集器,由於前者對全部代執行垃圾收集,然後者指對年輕代執行垃圾收集。
在J2SE 5.0的發行版中,若應用程序是運行在服務器類的機器上,則會默認使用並行垃圾收集器。在其餘機器上,能夠經過 -XX:+UseParallelGC參數來顯式啓用並行垃圾收集器。
並行整理垃圾收集器是在J2SE 5.0 update 6中被引入的,其與並行垃圾收集器的區別在於,並行整理垃圾收集器使用了新的算法對老年代進行垃圾收集。注意,最終,並行整理垃圾收集器會取代並行垃圾收集器。
年輕代中,並行整理垃圾收集器使用了與並行垃圾收集器相同的垃圾收集算法。
當使用並行整理垃圾收集時,老年代和永生代會使用stop-the-world的方式執行垃圾收集,大多數的並行模式都會使用移動整理(sliding compaction)。垃圾收集分爲三個階段。首先,將每個代從邏輯上分爲固定大小的區域。
在 標記階段(mark phase),應用程序代碼能夠直接到達的live對象的初始集合會被劃分到各個垃圾收集線程中,而後,全部的live對象會被並行標記。若一個對象被標記爲live,則會更新該對象所在的區域中與該對象的大小和位置相關的數據。
在 總結階段(summary phase)會對區域,而非單獨的對象進行操做。因爲以前的垃圾收集執行了整理,每一代的左側部分的對象密度會較高,包含了大部分live對象。這些對象密度較高的區域被恢復爲可用後,就不值得再花時間去整理了。因此,在總結階段要作的第一件事是從最左端對象開始檢查每一個區域的live對象密度,直到找到了一個恢復其本區域和恢復其右側的空間的開銷都比較小時中止。找到的區域的左側全部區域被稱爲dense prefix,不會再有對象被移動到這些區域裏了。這個區域後側的區域會被整理,清除全部已死的空間(清理垃圾對象佔用的空間)。總結階段會計算並保存每一個整理後的區域中對象的新地址。注意,在當前實現中,總結階段是串行的;固然總結階段也能夠實現爲並行的,但相對於性能總結階段的並行不及標記整理階段來得重要。
在 整理階段(compaction phase),垃圾收集線程使用總結階段收集到的數據決定哪些區域課餘填充數據,而後各個線程獨立的將數據拷貝到這些區域中。這樣就產生了一個底端對象密度大,連一端是一個很大的空區域塊的堆。
相對於並行垃圾收集器,使用並行整理垃圾收集器對那些運行在多CPU的應用程序更有好處。此外,老年代垃圾收集的並行操做能夠減小應用程序的暫停時間,對於那些對暫停時間有較高要求的應用程序來講,並行整理垃圾程序比並行垃圾收集更加適用。並行整理垃圾收集程序可能並不適用於那些與其餘不少應用程序並存於一臺機器的應用程序上,這種狀況下,沒有一個應用程序能夠獨佔全部的CPU。在這樣的機器上,須要考慮減小執行垃圾收集的線程數(使用-XX:ParallelGCThreads=n命令行選項),或者使用另外一種垃圾收集器。
若你想使用並行整理垃圾收集器,你必須顯式指定-XX:+UseParallelOldGC命令行選項。
對於不少應用程序來講,點到點的吞吐量並不如快速響應來的重要。典型狀況下,年輕代的垃圾收集並不會引發較長時間的暫停。可是,老年代的垃圾收集,雖不頻繁,卻可能引發長時間的暫停,特別是使用了較大的堆的時候。爲了應付這種狀況,HotSpot JVM使用了CMS垃圾收集器,也稱爲低延遲(low-latency)垃圾收集器。
CMS垃圾收集器只對老年代進行收集,年輕代實際默認使用ParNewGC
(一種年輕代的並行垃圾收集器)收集。
大部分老年代的垃圾收集使用了CMS
垃圾收集器,垃圾收集工做是與應用程序的執行併發進行的。
過程 | 描述 |
---|---|
初始標記 | 標記老年代的存活對象,也可能包括年輕代的存活對象。暫停應用線程stop-the world |
併發標記 | 和應用程序一塊兒執行,標記應用程序運行過程當中產生的存活的對象。 |
重標記 | 標記因爲應用程序更新致使遺漏的對象,暫停應用線程stop-the world |
併發清理 | 清理沒有被標記的對象,不會進行內存整理,可能致使內存碎片問題。 |
復位 | 清理數據等待下一次收集執行。 |
圖7展現了使用串行化的標記清理垃圾收集器和使用CMS垃圾收集器對老年代進行垃圾收集的區別。
不進行內存空間整理節省了時間,可是可用空間再也不是連續的了,垃圾收集也不能簡單的使用指針指向下一次可用來爲對象分配內存的地址了。相反,這種狀況下,須要使用可用空間列表。即,會建立一個指向未分配區域的列表,每次爲對象分配內存時,會從列表中找到一個合適大小的內存區域來爲新對象分配內存。這樣作的結果是,老年代上的內存的分配比簡單實用碰撞指針分配內存消耗大。這也會增長年輕代垃圾收集的額外負擔,由於老年代中的大部分對象是在新生代垃圾收集的時候重新生代提高爲老年代的。
使用CMS垃圾收集器的另外一個缺點是它所須要的對空間比其餘垃圾收集器大。在標記階段,應用程序能夠繼續運行,能夠繼續分配內存,潛在的可能會持續的增大老年代的內存使用。此外,儘管垃圾收集器保證會在標記階段標記出全部的live對象,可是在此階段中,某些對象可能會變成垃圾對象,這些對象不會被回收,直到下一次垃圾收集執行。這些對象成爲 浮動垃圾對象(floating garbage)。
最後,因爲沒有使用整理,會形成內存碎片的產生。爲了解決這個問題,CMS垃圾收集器會跟蹤經常使用對象的大小,預估可能的內存須要,可能會差分或合併內存塊來知足須要。
與其餘的垃圾收集器不一樣,當老年代被填滿後,CMS垃圾收集器並不會對老年代進行垃圾收集。相反,它會在老年代被填滿以前就執行垃圾收集工做。不然這就與串行或並行垃圾收集器同樣會形成應用程序長時間地暫停。爲了不這種狀況,CMS垃圾收集器會基於統計數字來來定執行垃圾收集工做的時間,這個統計數字涵蓋了前幾回垃圾收集的執行時間和老年代中新增內存分配的速率。當老年代中內存佔用率超過了稱爲初始佔用率的閥值後,會啓動CMS垃圾收集器進行垃圾收集。初始佔用率能夠經過命令行選項-XX:CMSInitiatingOccupancyFraction=n
進行設置,其中n是老年代佔用率的百分比的值,默認爲68。
整體來看,與平行垃圾收集器相比,CMS減小了執行老年代垃圾收集時應用暫停的時間,但卻增長了新生代垃圾收集時應用暫停的時間、下降了吞吐量並且須要佔用更大的堆空間。
G1最爲新一代的垃圾回收器,設計之初就是爲了取代CMS
的。具有如下優勢:
G1收集器和以前垃圾收集器擁有徹底不一樣的內存結構,雖然從邏輯上也存在年輕代、老年代,可是物理空間上不在連續而是散列在內存中的一個個regions
。內存空間分割成不少個相互獨立的空間,被乘稱做regions
。當jvm
啓動時regins
的大小就被肯定了。jvm會建立大概2000個regions,每一個region的大小在1M~32M之間。內存結構以下圖:
當年輕代GC被觸發時,Eden中存活的對象將會被複制或者移動evacuated
到倖存區的regions
,在倖存複製的次數到達閾值的存活對象將會晉升到老年區。這個過程也是一個Stop-the-world 暫停。eden和survivor的大小將會在下一次年輕代GC前從新計算。
一、內存被分割成相互獨立的大小相等的regions。 二、年輕代散列在整個內存空間中,這樣作的好處是當須要從新分配年輕代大小時會很是方便。 三、stop-the-word 暫停全部線程。 四、實際上也是並行回收算法,多線程並行收集。 五、存活的對象將被複制到新的 survivor或老年代 regions。
過程 | 描述 |
---|---|
初始標記 | stop-the world 一般伴隨在年輕代GC後面,標記有被老年代對象關聯的倖存區 regions |
掃描根 Regions | 和應用線程併發執行,掃描倖存區regions |
併發標記 | 併發標記整個堆存活的對象 |
重標記 | 完成整個堆的存活對象標記,使用snapshot-at-the-beginning (SATB) 算法標記存活對象,該算法比CMS中使用的更快。stop-the-word |
並行清理 | 並行清理死亡的的對象,返回空的regoins到可用列表。 |
複製 | 複製存活的對象到新的regions,This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)]. |
一、不要指定年輕代大小 -Xmn
,G1每次垃圾收集結束後都會重新計算並設置年輕代的大小,將會影響全局的暫停時間 二、響應時間配置 -XX:MaxGCPauseMillis=<N>
三、如何解決清理 or 複製失敗問題,經過增長-XX:G1ReservePercent=n
配置預留空間的大小,防止Evacuation Failure
,默認值是10.也可使用-XX:ConcGCThreads=n
增長併發標記的線程數來解決
選擇垃圾回收
-XX:+UseSerialGC 串行垃圾收集器
-XX:+UseParallelGC 並行垃圾收集器
-XX:+UseParallelOldGC 並行整理垃圾收集器
-XX:+UseConcMarkSweepGC 併發標記清理(CMS)垃圾收集年輕代默認使用-XX:+ParNewGC
-XX:+UserG1GC
複製代碼
查看垃圾收集日誌
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
複製代碼
對大小配置:
-Xmsn 堆最小值
-Xmxn 堆最大值
-Xmn 年輕代大小
-XX:NewRatio=n 年清代比例 Client_JVM=2 Server_JVM=8
-XX:SurvivorRatio=n 倖存去比例
-XX:MaxPermSize=n 依賴於不一樣平臺的實現永生代的最大值(java 8 之後啓用)。
複製代碼
G1可用的配置
-XX:+UseG1GC Use the Garbage First (G1) Collector
-XX:MaxGCPauseMillis=n Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it.
-XX:InitiatingHeapOccupancyPercent=n Percentage of the (entire) heap occupancy to start a concurrent GC cycle. It is used by GCs that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (e.g., G1). A value of 0 denotes 'do constant GC cycles'. The default value is 45.
-XX:NewRatio=n Ratio of new/old generation sizes. The default value is 2.
-XX:SurvivorRatio=n Ratio of eden/survivor space size. The default value is 8.
-XX:MaxTenuringThreshold=n Maximum value for tenuring threshold. The default value is 15.
-XX:ParallelGCThreads=n Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running.
-XX:ConcGCThreads=n Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running.
-XX:G1ReservePercent=n Sets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10.
-XX:G1HeapRegionSize=n
複製代碼
參考: