垃圾回收場景
新生代GC場景
在jvm記憶體模型中,新生代的記憶體分為為Eden和兩個Survivor
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-EChoB9sV-1620203260171)(F:\學習\學習筆記\jvm\images\image-20210429213855034.png)]
在系統不停的運作過程中,Eden區會被塞滿,這個時候就會觸發Minor GC,進行垃圾回收有專門的垃圾回收線程,不同的記憶體區域會有不同的垃圾回收器,相當于垃圾回收線程和垃圾回收器配合起來,使用自己的垃圾回收算法,對指定的記憶體區域進行垃圾回收,如下圖所示:
針對新生代采用ParNew垃圾回收器來進行回收,然後ParNew垃圾回收器針對新生代采用的就是複制算法來垃圾回收
這個時候垃圾回收器,就會把Eden區中的存活對象都标記出來,然後全部轉移到Survivor1去,接着一次性清空掉Eden中的垃圾對象
當Eden再次塞滿的時候,就又要觸發Minor GC了,此時已然是垃圾回收線程運作垃圾回收器中的算法邏輯,也就是采用複制算法邏輯,去标記出來Eden和Survivor1中的存活對象,然後一次性把存活對象轉移到Survivor2中去,接着把Eden和Survivor1中的垃圾對象都回收掉
-
在發生GC的時候,我們寫好的JAVA系統在運作期間還能不能繼續在新生代裡建立新的對象?
假如在GC期間,允許建立新的對象,那麼垃圾回收器在把Eden和Survivor1裡的存活對象标記轉移到Survivor2去,然後還在想辦法把Eden和Survivor1裡的垃圾對象都清理掉,結果這個時候系統程式還在不停的在Eden裡建立新的對象,那麼這些新對象很快就成了垃圾對象,有的還有人引用是存活對象,這對垃圾回收器完全亂套,一邊回收一邊還在建立新的對象。
Stop the World
JVM最大的痛點,就是垃圾回收的過程,在垃圾回收的時候,盡可能讓垃圾回收器專心的工作,不能随便讓我們的Java應用繼續建立對象,是以此時JVM會在背景進入“入“Stop the World”狀态,也就是說會直接停止我們的Java系統的所有工作線程,讓我們的代碼不再運作
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-ln3fq8MW-1620203260190)(F:\學習\學習筆記\jvm\images\StopTheWorld.png)]
這樣的話,就可以讓我們的系統暫停運作,然後不再建立新的對象,同時讓垃圾回收線程盡快完成垃圾回收的工作,就是标記和轉移Eden以及Survivor1的存活對象到Survivor2中去,然後盡快一次性回收掉Eden和Survivor1中的垃圾對象,等垃圾回收完畢後,繼續恢複我們寫的Java系統的工作線程,然後繼續運作我們的代碼邏輯,繼續在Eden區建立新的對象
Stop the World造成的系統停頓
在運作GC的時候會無法建立新的對象,則會造車系統停頓,如果Minor GC要運作50ms,則可能會導緻我們的系統在50ms内不能接受任何請求,在這50ms期間使用者發起的所有請求都會出現短暫的卡頓,因為系統的工作線程不在運作,不能處理請求
可能由于記憶體配置設定不合理,導緻對象頻繁進入老年代,平均七八分鐘一次Full GC,而Full GC比較慢,一次回收可能需要幾秒甚至幾十秒,是以一旦頻繁的Full GC,就會造成系統每隔幾分鐘卡死個幾十秒,讓使用者體驗極差
是以說,無論是新生代GC還是老年代GC,都盡量不要讓頻率過高,也避免持續時間過長,避免影響系統正常運作,這也是使用JVM過程中一個最需要優化的地方,也是最大的一個痛點。
不同的垃圾回收器的不同的影響
Serial垃圾回收器(新生代)
- 用一個線程進行垃圾回收,然後此時暫停系統工作線程
- 一般我們在伺服器程式中很少用這種方式
ParNew垃圾回收器(新生代)
- 常用的新生代垃圾回收器
- 針對伺服器一般都是多核CPU做了優化,他是支援多線程個垃圾回收的,可以大幅度提升回收的性能,縮短回收的時間
垃圾回收器
Serial和Serial Old垃圾回收器
- 分别用來回收新生代和老年代的垃圾對象
- 工作原理就是單線程運作,垃圾回收的時候會停止我們自己寫的系統的其他工作線程,讓我們系統直接卡死不動,然後讓他們垃圾回收,這個現在一般寫背景Java系統幾乎不用。
ParNew和CMS垃圾回收器
- ParNew現在一般都是用在新生代的垃圾回收器,采用的就是複制算法來垃圾回收
- CMS是用在老年代的垃圾回收器
- 都是多線程并發的機制,性能更好,現在一般是線上生産系統的标配組合
ParNew
理論
- 沒有最新的G1垃圾回收器的話,通常大家線上系統都是ParNew垃圾回收器作為新生代的垃圾回收器當然現在即使有了G1,其實很多線上系統還是用的ParNew
- 通常運作在伺服器上Java系統,都可以充分利用伺服器的多核CPU優勢,如果對新生代回收的時候,僅僅使用單線程進行垃圾回收,會導緻浪費CPU的資源
-
新生代的ParNew垃圾回收器主打的就是多線程垃圾回收機制,另外一種Serial垃圾回收器主打的是單線程垃
圾回收,他們倆都是回收新生代的,唯一的差別就是單線程和多線程的差別,但是垃圾回收算法是完全一樣
- ParNew垃圾回收器如果一旦在合适的時機執行Minor GC的時候,就會把系統程式的工作線程全部停掉,禁止程式繼續運作建立新的對象,然後自己就用多個垃圾回收線程去進行垃圾回收,回收的機制和算法都是一樣的
參數設定
部署到Tomcat時可以在Tomcat的catalina.sh中設定Tomcat的JVM參數,使用Spring Boot也可以在啟動時指定JVM參數。
- 指定使用ParNew垃圾回收器
使用“-XX:+UseParNewGC”選項,隻要加入這個選項,JVM啟動之後對新生代進行垃圾回收的,就是ParNew垃圾回收器
-
ParNew垃圾回收器預設情況下的線程數量
一旦我們指定了使用ParNew垃圾回收器之後,他預設給自己設定的垃圾回收線程的數量就是跟CPU的核數是一樣的
如果你一定要自己調節ParNew的垃圾回收線程數量,也是可以的,使用“-XX:ParallelGCThreads”參數即可,
通過他可以設定線程的數量
CMS
理論
- 老年代選擇的垃圾回收器是CMS,他采用的是标記清理算法
- 标記清理算法:先通過GC Roots的方法,看各個對象是否被GC Roots給引用,如果是的話,那就是存活對象,否則就是垃圾對象。先将垃圾對象都标記出來,然後一次性把垃圾對象都回收掉,這種方法最大問題:就是會造成很多記憶體碎片,這種記憶體碎片不大不小,可能放不下任何一個對象,則會造成記憶體浪費
- CMS的STW(Stop the World)問題:如果停止一切工作線程,然後慢慢的執行“标記-清理”算法,會導緻系統卡死時間過長,很多響應無法處理。是以CMS垃圾回收器采取的是:垃圾回收線程和系統工作線程盡量同時執行的模式來處理
如何實作系統一邊工作的同時進行垃圾回收?
CMS在執行一次垃圾回收的過程共分為4個階段:
- 初始标記
- 并發标記
- 重新标記
- 并發清理
1、初始标記
CMS在進行垃圾回收時,會先執行初始标記階段。這個階段會讓系統的工作線程全部停止,進入“Stop The World”狀态,初始标記執行STW影響不大,因為他的速度比較快,隻是标記出GC Roots直接應用的對象
2、并發标記
這個階段會讓系統可以随意建立各種新對象,繼續運作,在運作期間可能會建立新的存活對象,也可能會讓部分存活對象失去引用,變成垃圾對象。在這個過程中,垃圾回收線程,會盡可能的對已有的對象進行GC Roots追蹤,但是這個過程中,在進行并發标記的時候,系統程式會不停的工作,他可能會各種建立出來新的對象,部分對象可能成為垃圾
這個階段就是對老年代所有對象進行GC Roots追蹤,其實是最耗時的,需要追蹤所有對象是否從根源上被GC Roots引用了,但是這個最耗時的階段,是跟系統程式并發運作的,是以其實這個階段不會對系統運作造成影響。
3、重新标記
因為第二階段裡,你一邊标記存活對象和垃圾對象,一邊系統在不停運作建立新對象,讓老對象變成垃圾,是以第二階段結束之後,絕對會有很多存活對象和垃圾對象,是之前第二階段沒标記出來的。是以此時進入第三階段,要繼續讓系統程式停下來,**再次進入“Stop the World”階段。**然後重新标記下在第二階段裡新建立的一些對象,還有一些已有對象可能失去引用變成垃圾的情況
這個重新标記的階段,是速度很快的,他其實就是對在第二階段中被系統程式運作變動過的少數對象進行标記,是以運作速度很快,接着重新恢複系統程式的運作。
4、并發清理
讓系統程式随意運作,然後他來清理掉之前标記為垃圾的對象,這個階段比較耗時,需要進行對象的清理,但是他是跟着系統程式并發運作的,是以也不影響系統程式的執行
CMS垃圾回收器問題
1、并發回收導緻CPU資源緊張
CMS垃圾回收器有一個最大的問題,雖然能在垃圾回收的同時讓系統同時工作,在并發标記和并發清理兩個最耗時的階段,垃圾回收線程和系統工作線程同時工作,會導緻有限的CPU資源被垃圾回收線程占用了一部分
并發标記的時候,需要對GC Roots進行深度追蹤,看所有對象裡面到底有多少人是存活的但是因為老年代裡存活對象是比較多的,這個過程會追蹤大量的對象,是以耗時較高。并發清理,又需要把垃圾對象從各種随機的記憶體
位置清理掉,也是比較耗時的
是以在這兩個階段,CMS的垃圾回收線程是比較耗費CPU資源的。CMS預設啟動的垃圾回收線程的數量是(CPU核數 + 3)/ 4
2、Concurrent Mode Failure問題
在并發清理階段,CMS隻不過是回收之前标記好的垃圾對象,但是這個階段系統一直在運作,可能會随着系統運作讓一些對象進入老年代,同時還變成垃圾對象,這種垃圾對象是“浮動垃圾”。因為他雖然成為了垃圾,但是CMS隻能回收之前标記出來的垃圾對象,不會回收他們,需要等到下一次GC的時候才會回收他們。是以為了保證在CMS垃圾回收期間,還有一定的記憶體空間讓一些對象可以進入老年代,一般會預留一些空間。CMS垃圾回收的觸發時機,其中有一個就是當老年代記憶體占用達到一定比例了,就自動執行GC。
“-XX:CMSInitiatingOccupancyFaction”參數可以用來設定老年代占用多少比例的時候觸發CMS垃圾回收,JDK 1.6裡面預設的值是92%
也就是說,老年代占用了92%空間了,就自動進行CMS垃圾回收,預留8%的空間給并發回收期間,系統程式把一些新對象放入老年代中。
-
那麼如果CMS垃圾回收期間,系統程式要放入老年代的對象大于了可用記憶體空間,此時會如何?
這個時候,會發生Concurrent Mode Failure,就是說并發垃圾回收失敗了,我一邊回收,你一邊把對象放入老年代,記憶體都不夠
此時就會自動用“Serial Old”垃圾回收器替代CMS,就是直接強行把系統程式“Stop the World”,重新進行長時間的GC Roots追蹤,标記出來全部垃圾對象,不允許新的對象産生,然後一次性把垃圾對象都回收掉,完事後再恢複系統線程
3、記憶體碎片問題
老年代的CMS采用“标記-清理”算法,每次都是标記出來垃圾對象,然後一次性回收掉,這樣會導緻大量的記憶體碎片産生。如果記憶體碎片太多,會導緻後續對象進入老年代找不到可用的連續記憶體空間了,然後觸發Full GC
是以CMS不是完全就僅僅用“标記-清理”算法的,因為太多的記憶體碎片實際上會導緻更加頻繁的Full GC
CMS有一個參數是“-XX:+UseCMSCompactAtFullCollection”,預設就打開,意思是在Full GC之後要再次進行“Stop the World”,停止工作線程,然後進行碎片整理,就是把存活對象挪到一起,空出來大片連續記憶體空間,避免記憶體碎片
還有一個參數是“-XX:CMSFullGCsBeforeCompaction”,這個意思是執行多少次Full GC之後再執行一次記憶體碎片整理的工作,預設是0,意思就是每次Full GC之後都會進行一次記憶體整理
觸發老年代GC的時機
- 1、老年代可用記憶體小于新生代全部對象的大小,如果沒開啟空間擔保參數,會直接觸發Full GC,是以一般空間擔保參數都會打開;
- 2、老年代可用記憶體小于曆次新生代GC後進入老年代的平均對象大小,此時會提前Full GC;
- 3、新生代Minor GC後的存活對象大于Survivor,那麼就會進入老年代,此時老年代記憶體不足;
- 4、-XX:CMSInitiatingOccupancyFaction:老年代的已用記憶體大于設定的閥值,就會觸發Full GC;
- 5、顯示調用System.gc
ParNew + CMS帶給我們的痛點是什麼
Stop the World,這個是大家最痛的一個點
無論是新生代垃圾回收,還是老年代垃圾回收,都會或多或少産生“Stop the World”現象,對系統的運作是有一定影響的。是以其實之後對垃圾回收器的優化,都是朝着減少“Stop the World”的目标去做的。
在這個基礎之上,G1垃圾回收器就應運而生了,他可以提供比“ParNew + CMS”組合更好的垃圾回收的性能
G1垃圾回收器
特點
G1垃圾回收器是可以同時回收新生代和老年代的對象的,不需要兩個垃圾回收器配合起來運作,他一個人就可以搞定所有的垃圾回收。
-
1、把Java堆記憶體拆分為多個大小相等的Region
G1也會有新生代和老年代的概念,但是隻不過是**邏輯上的概念**
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-dxxCxrNt-1620203260199)(F:\學習\學習筆記\jvm\images\G1 region.png)]
也就是說新生代可能包含了某些Region,老年代可能包含了某些Region。
-
2、可以設定一個垃圾回收的預期停頓時間
也就是說比如我們可以指定:希望G1在垃圾回收的時候,可以保證,在1小時内由G1垃圾回收導緻的“Stop the World”時間,也就是系統停頓的時間,不能超過1分鐘,這樣相當于我們就可以直接控制垃圾回收對系統性能的影響
-
3、Region可能屬于新生代也可能屬于老年代
剛開始Region可能誰都不屬于,然後接着就配置設定給了新生代,然後放了很多屬于新生代的對象,接着就觸發了垃圾回收這個Region,下一次同一個Region可能又被配置設定了老年代了,用來放老年代的長生存周期的對象,是以其實在G1對應的記憶體模型中,Region随時會屬于新生代也會屬于老年代,是以沒有所謂新生代給多少記憶體,老年代給多少記憶體這一說
實際上新生代和老年代各自的記憶體區域是不停的變動的,由G1自動控制
G1是如何做到對垃圾回收導緻的系統停頓可控的?
其實G1如果要做到這一點,他就必須要追蹤每個Region裡的回收價值,啥叫做回收價值呢?
他必須搞清楚每個Region裡的對象有多少是垃圾,如果對這個Region進行垃圾回收,需要耗費多長時間,可以回收掉多少垃圾?G1通過追蹤發現,1個Region中的垃圾對象有10MB,回收他們需要耗費1秒鐘,另外一個Region中的垃圾對象有20MB,回收他們需要耗費200毫秒。
然後在垃圾回收的時候,G1會發現在最近一個時間段内,比如1小時内,垃圾回收已經導緻了幾百毫秒的系統停頓了,現在又要執行一次垃圾回收,那麼必須是回收上圖中那個隻需要200ms就能回收掉20MB垃圾的Region;于是G1觸發一次垃圾回收,雖然可能導緻系統停頓了200ms,但是一下子回收了更多的垃圾,就是20MB的垃圾
是以簡單來說,G1可以做到讓你來設定垃圾回收對系統的影響,他自己通過把記憶體拆分為大量小Region,以及追蹤每個Region中可以回收的對象大小和預估時間,最後在垃圾回收的時候,盡量把垃圾回收對系統造成的影響控制在你指定的時間範圍内,同時在有限的時間内盡量回收盡可能多的垃圾對象。這就是G1的核心設計思路
如何設定G1對應的記憶體大小?
- G1對應的是一大堆的Region記憶體區域,每個Region的大小是一緻的,預設情況下自動計算和設定的,可以給整個堆記憶體設定一個大小,比如說用“-Xms”和“-Xmx”來設定堆記憶體的大小
- JVM啟動的時候,發現使用的是G1垃圾回收器(通過:用“-XX:+UseG1GC”來指定使用G1垃圾回收器),此時會自動用堆大小除以2048,JVM最多可以有2048個Region,然後Region的大小必須是2的倍數,比如說1MB、2MB、4MB之類,可以通過手動方式來指定,則是**“-XX:G1HeapRegionSize“**
- 剛開始的時候,預設新生代對堆記憶體的占比是5%,這個是可以通過“-XX:G1NewSizePercent”來設定新生代初始占比的,其實維持這個預設值即可
- 在系統運作中,JVM其實會不停的給新生代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”,而且一旦Region進行了垃圾回收,此時新生代的Region數量還會減少,這些其實都是動态
新生代還有Eden和Survivor的概念?
- G1中雖然把記憶體劃分為很多的 Region,但是其實還是有新生代、老年代的區分,而且新生代裡還是有Eden和Survivor的劃分
- 通過參數,“-XX:SurvivorRatio=8”,可以設定新生代中80%的Region屬于Eden,兩個Survivor各自占10%
- 随着對象不停的在新生代裡配置設定,屬于新生代的Region會不斷增加,Eden和Survivor對應的Region也會不斷增加
G1的新生代垃圾回收觸發機制?
既然G1的新生代也有Eden和Survivor的區分,那麼觸發垃圾回收的機制都是類似的,随着不停的在新生代的Eden對應的Region中放對象,JVM就會不停的給新生代加入更多的Region,直到新生代占據堆大小的最大比例60%。
一旦新生代達到了設定的占據堆記憶體的最大大小60%,這個時候還是會觸發新生代的GC,G1就會用之前說過的複制算法來進行垃圾回收,進入一個“Stop the World”狀态,然後把Eden對應的Region中的存活對象放入S1對應的Region中,接着回收掉Eden對應的Region中的垃圾對象,但是這個過程跟之前是有差別的,因為G1是可以設定目标GC停頓時間的,也就是G1執行GC的時候最多可以讓系統停頓多長時間,可以通過“-XX:MaxGCPauseMills”參數來設定,預設值是200ms。
那麼G1就會通過之前說的,對每個Region追蹤回收他需要多少時間,可以回收多少對象來選擇回收一部分的Region,保證GC停頓時間控制在指定範圍内,盡可能多的回收掉一些對象。
對象什麼時候進入老年代?
可以說跟之前幾乎是一樣的,還是這麼幾個條件:
1、對象在新生代躲過了很多次的垃圾回收,達到了一定的年齡了,“-XX:MaxTenuringThreshold”參數可以設定這個年齡,就會進入老年代
2、 動态年齡判定規則,如果一旦發現某次新生代GC過後,存活對象超過了Survivor的50%
大對象Region
- 在之前,大對象是直接進入老年代,在G1的記憶體模型中,G1提供了專門的Region來存放大對象,而不是讓大對象直接進入老年的Region中。
- 在G1中,大對象的判定規則就是一個大對象超過了一個Region大小的50%,如果每個Region是2MB,隻要一個大對象超過了1MB,就會被放入大對象專門的Region中,而且一個大對象如果太大,可能會橫跨多個Region來存放
- 在新生代、老年代回收的時候,會順帶帶着大對象Region一起回收