方法的局部變量、類的靜態變量。
(1)強引用(即最普通的對象引用)對象:在垃圾回收的時候是絕對不會被回收的;(2)軟引用(SoftReference)對象:正常狀況下垃圾回收是不會回收軟引用對象的,可是若是進行垃圾回收以後,發現內存空間仍是不夠存放新的對象,內存都快溢出了,此時就會把這些軟引用對象給回收掉,哪怕他被變量引用了,可是由於它是軟引用,因此仍是要回收;linux
(3)弱引用(WeakReference)對象:弱引用對象跟沒引用似的,只要發生垃圾回收就會被回收掉;web
(1)躲過15次年輕代GC以後進入老年代:對象每次在新生代裏躲過一次GC被轉移到一塊Survivor區域中,它的年齡就會增加一歲。默認的設置下,當對象的年齡達到15歲的時候,也就是躲過15次Young GC的時候,它就會被轉移到老年代裏面去。(2)動態對象年齡判斷:年齡1+年齡2+年齡n(年齡從小到大進行累加)的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n及以上的對象都放入老年代。不管15歲的那個規則,仍是動態年齡判斷的規則,都是但願那些多是長期存活的對象,儘早進入老年代。算法
(3)大對象直接進入老年代:之因此這麼作,就是爲了不新生代裏出現那種大對象,而後多次躲過GC,還得把它在兩個Survivor區域裏來回複製屢次以後纔會進入老年代。數據庫
(4)Young GC後的對象太多沒法放入Survivor區:這個時候就必須得把這些對象直接轉移到老年代去。瀏覽器
若是Young GC後新生代裏有大量對象存活下來,確實是本身的Survivor區放不下了,必須轉移到老年代去,那麼若是老年代裏空間也不夠放這些對象呢?首先,在執行任何一次Young GC以前,JVM會先檢查一下老年代裏可用的內存空間(最大可用連續內存空間),是否大於新生代全部對象的總大小。爲啥檢查這個呢?由於最極端的狀況下,可能新生代Young GC事後,全部對象都存活下來了,那豈不是新生代全部對象所有要進入老年代?性能優化
若是發現老年代的可用內存大小是大於新生代全部對象的,此時就能夠放心大膽的對新生代發起一次Young GC了,由於即便Young GC以後全部對象都存活,Survivor區放不下了,也能夠轉移到老年代裏去。服務器
可是假如執行Young GC以前,發現老年代的可用內存已經小於新生代的所有對象大小了,那麼這個時候是否是有可能在Young GC以後新生代的對象所有存活下來,而後所有須要轉移到老年代去,可是老年代空間又不夠?理論上,是有這種可能的。併發
若是設置了虛擬機參數HandlePromotionFailure,則會繼續嘗試進行下一步判斷。jvm
下一步判斷,就是看看老年代的可用內存大小,是否大於以前每一次Young GC後進入老年代的對象的平均大小。高併發
若是上面那個步驟判斷失敗了,或者是"-XX:-HandlePromotionFailure"參數沒設置,此時就會直接觸發一次"Full GC",就是對老年代進行垃圾回收,儘可能騰出來一些內存空間,而後再執行Young GC。若是上面兩個步驟都判斷成功了,那麼就是說能夠冒點風險嘗試一下Young GC。此時進行Young GC有幾種可能結果:
第一種可能,Young GC事後,剩餘的存活對象的大小,是小於Survivor區域的大小的,那麼此時存活對象進入Survivor區域便可。
第二種可能,Young GC事後,剩餘的存活對象的大小,是大於Survivor區域的大小的,可是是小於老年代可用內存大小的,此時就直接進入老年代便可。
第三種可能,很不幸,Young GC事後,剩餘的存活對象的大小,大於了Survivor區域的大小,也大於了老年代可用內存的大小。此時老年代都放不下這些存活對象了,就會發生"Handle Promotion Failure"的狀況,這個時候就會觸發一次"Full GC"。
Full GC就是對老年代進行垃圾回收,同時也通常會對新生代進行垃圾回收。由於這個時候必須得把老年代裏的沒人引用的對象給回收掉,而後纔可能讓Young GC事後剩餘的存活對象進入老年代裏面。
若是Full GC事後,老年代仍是沒有足夠的空間存放Young GC事後的剩餘存活對象,那麼此時就會致使OOM內存溢出了,由於內存實在是不夠了,你還要不停的往裏面放對象,固然就崩潰了。
CMS執行一次垃圾回收的過程一共分爲4個階段:初始標記、併發標記、從新標記、併發清理。首先,CMS要進行垃圾回收時,會先執行初始標記階段,這個階段會讓系統的工做線程所有中止,進入"Stop the World"狀態。所謂的"初始標記"是說標記出來全部GC Roots直接引用的對象,即方法的局部變量和類的靜態變量,而類的實例變量不是GC Roots。初始標記雖然要"Stop the World"暫停一切工做線程,但其實影響不大,由於它的速度很快,僅僅標記GC Roots直接引用的那些對象便可。
接着第二個階段,是併發標記,這個階段會讓系統線程能夠隨意建立各類新對象,繼續運行。在運行期間可能會建立新的存活對象,也可能會讓部分存活對象失去引用,變成垃圾對象。在這個過程當中,垃圾回收線程,會盡量的對已有的對象進行GC Roots追蹤。併發標記會對老年代全部對象進行GC Roots追蹤,實際上是最耗時的,它須要追蹤全部對象是否從根源上被GC Roots引用了,可是這個最耗時的階段,是跟系統程序併發運行的,因此這個階段不會對系統運行形成影響的。
接着會進入第三個階段,從新標記階段。由於在第二階段裏,你一邊標記存活對象和垃圾對象,一邊系統在不停運行建立新對象,或者讓部分老對象變成垃圾對象,因此第二階段結束以後,絕對會有不少存活對象和垃圾對象,是以前第二階段沒標記出來的。因此此時進入第三階段,要繼續讓系統程序停下來,再次進入"Stop the World"狀態。這個從新標記的階段,是速度很快的,它其實就是對在第二階段中被系統程序運行所變更過的少數對象進行標記,因此運行速度很快。
接着從新恢復系統程序的運行,進入第四階段:併發清理。這個階段就是讓系統程序隨意運行,而後它來清理掉以前標記爲垃圾的對象便可。這個階段其實也是很耗時的,由於須要進行對象的清理,可是它也是跟系統程序併發運行的,因此其實也不影響系統程序的運行。
CMS的垃圾回收機制已經儘量的對垃圾回收進行了性能優化。由於最耗時的,其實就是對老年代所有對象進行GC Roots追蹤,標記出來到底哪些對象能夠回收;而後是把各類垃圾對象從內存裏清理掉,這兩個過程是最耗時的。可是它的第二階段和第四階段,都是和系統程序併發執行的,因此基本這兩個最耗時的階段對性能影響不大。只有第一個階段和第三個階段是須要"Stop the World"的,可是這兩個階段都是簡單的標記而已,速度很是的快,因此基本上對系統運行影響也不大。
CMS雖然能在垃圾回收的同時讓系統同時工做,可是在併發標記和併發清理這兩個最耗時的階段,垃圾回收線程和系統工做線程同時工做,會致使有限的CPU資源被垃圾回收線程佔用一部分。併發標記的時候,須要對GC Roots進行深度追蹤,看老年代全部對象裏面到底有多少是存活的,可是由於老年代裏存活對象是比較多的,這個過程會追蹤大量的對象,因此耗時較高。併發清理,又須要把垃圾對象從各類隨機的內存位置清理掉,也是比較耗時的。所以在這兩個階段,CMS的垃圾回收線程是比較耗費CPU資源的。CMS默認啓動的垃圾回收線程的數量是(CPU核數+3)/ 4。
在併發清理階段,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追蹤,標記出來所有垃圾對象,不容許新的對象產生,而後一次性把垃圾對象都回收掉,完事以後才恢復系統線程。因此在生產實踐中,這個自動觸發CMS垃圾回收的比例須要合理優化一下,避免"Concurrent Mode Failure"問題。
CMS是一款基於"標記-清理"算法實現的老年代垃圾收集器,每次都是標記出來垃圾對象,而後一次性回收掉,這樣會致使大量的內存碎片產生。若是內存碎片太多,就會致使後續對象進入老年代找不到可用的連續內存空間,而後不得不觸發Full GC。因此CMS不是徹底就僅僅用"標記-清理"算法的,由於太多的內存碎片實際上會致使更加頻繁的Full GC。CMS有一個參數是"-XX:+UseCMSCompactAtFullCollection",默認就打開了,意思是在Full GC以後要再次進行"Stop the World",中止工做線程,而後進行內存碎片整理,把存活對象挪到一塊兒,空出來大片連續內存空間,避免內存碎片。還有一個參數是"-XX:CMSFullGCsBeforeCompaction",這個意思是執行多少次Full GC以後再執行一次內存碎片整理的工做,默認是0,意思是每次Full GC以後都會進行一次內存碎片整理。
其實緣由很簡單,只要分析一下它們倆的執行過程就好了。新生代存活對象是不多的,從GC Roots出發不須要追蹤多少對象就好了,因此速度是很快的,而後直接把存活對象放入Survivor中,就一次性直接回收到Eden區和以前使用的Survivor區。可是CMS的Full GC呢?在併發標記階段,它須要去追蹤老年代裏全部存活對象,而老年代存活對象不少,這個過程就會很慢;其次併發清理階段,它不是一次性回收一大片內存,而是找到零零散散在各個地方的垃圾對象,速度也很慢;最後還得執行一次內存碎片整理,把大量的存活對象給挪到一塊兒,空出來連續內存空間,這個過程還得"Stop the World",那就更慢了。萬一併發清理期間,剩餘內存空間不足以存放要進入老年代的對象,引起了"Concurrent Mode Failure"問題,那更是麻煩,還得立馬用"Serial Old"垃圾回收器,"Stop the World"以後慢慢從新來一遍回收的過程,這更是耗時。因此綜上所述,老年代的垃圾回收,就是一個字:慢。
第一是老年代可用內存小於新生代所有對象的大小,若是沒開啓空間擔保參數,會直接觸發Full GC,因此通常空間擔保參數都會打開;第二是老年代可用內存小於歷次新生代GC後進入老年代的平均對象大小,此時會提早Full GC;
第三是新生代Minor GC後的存活對象大於Survivor,那麼就會進入老年代,此時若是老年代內存不足會觸發Full GC;
第四是老年代可用內存大於歷次新生代GC後進入老年代的平均對象大小,可是老年代已經使用的內存空間超過了"-XX:CMSInitiatingOccupancyFaction"參數指定的比例,也會自動觸發Full GC。
G1垃圾回收器是能夠同時回收新生代和老年代的對象的,不須要兩個垃圾回收器配合起來運做,它一我的就能夠搞定全部的垃圾回收。G1的一個顯著特色,是把Java堆內存拆分紅多個大小相等的Region。雖然它也有新生代和老年代的概念,可是隻不過是邏輯上的概念,也就是說,新生代可能包含了某些Region,老年代可能包含了某些Region。
G1另一個顯著的特色就是可讓咱們設置一個垃圾回收的預期停頓時間。不少JVM優化的思路其實就是對內存合理分配,優化一些參數,儘量減小Minor GC和Full GC的次數,減小GC帶來的系統停頓,避免影響系統處理請求。可是如今咱們直接能夠給G1指定,在一段時間內,垃圾回收致使的系統停頓時間不能超過多久,而後G1全權給你負責,保證達到這個目標,這就至關於咱們能夠直接控制垃圾回收對系統性能的影響了。
G1要作到垃圾回收對系統停頓可控,它就必需要追蹤每一個Region裏的回收價值。啥叫作回收價值呢?G1必須搞清楚每一個Region裏的對象有多少是垃圾,若是對這個Region進行垃圾回收,須要耗費多長時間,能夠回收掉多少垃圾。總結來講,G1垃圾回收器的設計思想,主要是把內存拆分爲不少個小的Region,而後新生代和老年代各自對應一些Region,追蹤每一個Region中能夠回收的對象大小和預估時間,回收的時候儘量挑選回收效率最高的Region,儘量保證達到咱們指定的垃圾回收時的系統停頓時間。
在G1中,每個Region是可能屬於新生代,可是也可能屬於老年代的。剛開始Region可能誰都不屬於,而後接着就被分配給了新生代,而後放了不少屬於新生代的對象,接着觸發了GC回收了這個Region;而後下一次同一個Region可能又被分配給了老年代了,用來放老年代的長生存週期的對象。因此在G1對應的內存模型中,Region隨時會屬於新生代或老年代,沒有所謂的新生代給多少內存,老年代給多少內存這一說。新生代和老年代各自的內存區域是不停在變更的,由G1自動控制。
首先思考兩個問題:G1到底劃分多少個Region?每一個Region的大小是多大?其實這個默認狀況下是自動計算和設置的,咱們能夠給整個堆內存設置一個大小,好比說用"-Xms"和"-Xmx"來設置堆內存的大小。而後JVM啓動的時候一旦發現你使用的是G1垃圾回收器(可使用"-XX:+UseG1GC"來指定使用G1垃圾回收器),此時會自動用堆大小除以2048,由於JVM最多能夠有2048個Region,而後Region的大小必須是2的倍數,好比說1MB、2MB、4MB之類的。好比說堆大小是4G,那麼就是4096MB,此時除以2048個Region,每一個Region的大小就是2MB。大概就是這樣子來決定Region的數量和大小的,通常保持默認的計算方式就能夠。
剛開始的時候,默認新生代對堆內存的佔比是5%,也就是佔據200MB左右的內存,對應大概是100個Region,這個是能夠經過"-XX:G1NewSizePercent"來設置新生代初始佔比的,維持這個默認值便可。由於在系統運行中,JVM其實會不停的給新生代增長更多的Region,可是最多新生代的佔比不會超過60%(能夠經過"-XX:G1MaxNewSizePercent"設置)。並且一旦Region進行了垃圾回收,此時新生代的Region數量會減小,這些都是動態的。
G1雖然把內存劃分紅了不少的Region,可是其實仍是有新生代、老年代的區分的,並且新生代裏仍是有Eden和Survivor的劃分的,它們會各自佔據不一樣的Region,隨着對象不停的在新生代裏分配,屬於新生代的Region會不斷增長,Eden和Survivor對應的Region也會不斷增長。
既然G1的新生代也有Eden和Survivor的區分,那麼觸發垃圾回收的機制都是相似的。隨着不停的在新生代的Eden對應的Region中放對象,JVM就會不停的給新生代加入更多的Region,直到新生代佔據堆大小的最大比例60%。一旦新生代達到了設定的佔據堆內存的最大大小60%,好比都有1200個Region了,裏面的Eden可能佔據了1000個Region,每一個Survivor是100個Region,並且Eden區還佔滿了對象,這個時候仍是會觸發新生代的GC的,G1會用複製算法來進行垃圾回收,進入一個"Stop the World"狀態,而後把Eden對應的Region中的存活對象放入S1對應的Region中,接着回收掉Eden對應的Region中的垃圾對象。可是這個過程跟其餘垃圾回收器是有區別的,由於G1是能夠設定目標GC停頓時間的,也就是能夠指定G1執行GC的時候最多可讓系統停頓多長時間,能夠經過"-XX:MaxGCPauseMills"參數來設定,默認值是200ms。那麼G1就會經過對每一個Region追蹤回收它須要多長時間,能夠回收多少對象來選擇回收一部分的Region,保證GC停頓時間控制在指定範圍內,儘量多的回收掉一些對象。
在G1的內存模型下,新生代和老年代各自都會佔據必定的Region,老年代也會有本身的Region,按照默認新生代最多隻能佔據堆內存60%的Region來推算,老年代最多能夠佔據40%的Region,大概就是800個左右的Region。那麼對象何時重新生代進入老年代呢?
(1)對象在新生代躲過了不少次的垃圾回收,達到了必定的年齡了,"-XX:MaxTenuringThreshold"參數能夠設置這個年齡,他就會進入老年代;
(2)動態年齡斷定規則,若是一旦發現某次新生代GC事後,存活對象超過了Survivor的50%。好比年齡1歲、2歲、3歲、4歲的對象的大小總和超過了Survivor的50%,此時4歲以上的對象所有會進入老年代,這就是動態年齡斷定規則。
通過一段時間的新生代使用和垃圾回收以後,總有一些對象會進入老年代中。
G1提供了專門的Region來存放大對象,而不是讓大對象進入老年代的Region中。在G1中,大對象的斷定規則是一個大對象超過了一個Region大小的50%,好比每一個Region是2MB,只要一個大對象超過了1MB,就會被放入大對象專門的Region中,並且一個大對象若是太大,可能會橫跨多個Region來存放。
那堆內存裏哪些Region用來存放大對象呢?不是說60%的給新生代,40%的給老年代嗎,那還有Region給大對象?很簡單,在G1裏,新生代和老年代的Region是不停的變化的,好比新生代如今佔據了1200個Region,可是一次垃圾回收以後,就讓裏面1000個Region都空了,此時那1000個Region就能夠不屬於新生代了,裏面不少Region能夠用來存放大對象。
大對象既然不屬於新生代和老年代,那何時會觸發垃圾回收呢?也很簡單,新生代、老年代在回收的時候,會順帶着大對象Region一塊兒回收,因此這就是在G1內存模型下對大對象的分配和回收的策略。
G1有一個參數,是"-XX:InitiatingHeapOccupancyPercent",它的默認值是45%,意思是若是老年代佔據了堆內存的45%的Region的時候,就會嘗試觸發一個新生代+老年代一塊兒回收的混合回收階段。好比堆內存有2048個Region,若是老年代佔據了其中45%的Region,也就是接近1000個Region的時候,就會觸發一個混合回收。
首先會觸發一個"初始標記"的操做,這個過程是須要進入"Stop the World"的,僅僅只是標記一下GC Roots直接能引用的對象,這個過程速度是很快的。先中止系統程序的運行,而後對各個線程棧內存中的局部變量表明的GC Roots,以及方法區中的類靜態變量表明的GC Roots,進行掃描,標記出來它們直接引用的那些對象。接着會進入"併發標記"的階段,這個階段會容許系統程序的運行,同時進行GC Roots追蹤,從GC Roots開始追蹤全部的存活對象。這個併發標記階段是很耗時的,由於要追蹤所有的存活對象。可是這個階段是能夠跟系統程序併發運行的,因此對系統程序的影響不太大。並且JVM會把併發標記階段對對象作出的修改操做記錄下來,好比哪一個對象被新建了,哪一個對象失去了引用。
接着是下一個階段,最終標記階段,這個階段會進入"Stop the World",系統程序是禁止運行的,可是會根據併發標記階段的對象修改操做記錄,最終標記一下有哪些存活對象,哪些垃圾對象。
最後一個階段,就是"混合回收"階段,這個階段會計算老年代中每一個Region中的存活對象數量,存活對象的佔比,還有執行垃圾回收的預期性能和效率。接着會中止系統程序,而後盡心盡力儘快進行垃圾回收,此時會選擇部分Region進行回收,由於必須讓垃圾回收的停頓時間控制在咱們指定的範圍內。好比老年代此時有1000個Region都滿了,可是由於根據預約目標,本次垃圾回收可能只能停頓200毫秒,那麼經過以前的計算得知,可能回收其中800個Region恰好須要200ms,那麼就只會回收800個Region,把GC致使的停頓時間控制在咱們指定的範圍內。
其實老年代對堆內存佔比達到45%時觸發的所謂"混合回收"不只僅回收老年代,還會回收新生代和大對象。那麼,究竟是回收這些區域中的哪些Region呢?那就要看狀況了,由於咱們設定了對GC停頓時間的目標,因此它會重新生代、老年代、大對象裏各自挑選一些Region,保證用指定的時間(好比200ms)回收儘量多的垃圾對象。
G1在老年代的Region佔據了堆內存的Region的45%以後,會觸發一個混合回收的過程,也就是Mixed GC,分爲了好幾個階段。其中最後一個階段是執行混合回收,重新生代和老年代裏都回收一些Region,但在最後一個階段混合回收的時候,它其實會中止全部程序運行的,因此G1是容許執行屢次混合回收的。好比先中止工做,執行一次混合回收回收掉一些Region,接着恢復系統運行,而後再次中止系統運行,再執行一次混合回收回收掉一些Region。有一些參數能夠控制其中的一些細節。好比"-XX:G1MixedGCCountTarget"參數,意思是在一次混合回收的過程當中,最後一個階段執行幾回混合回收,默認值是8次。意味着最後一個階段,先中止系統運行,混合回收一些Region,再恢復系統運行,接着再次禁止系統 運行,混合回收一些Region,反覆8次。假設一次混合回收預期要回收掉一共有160個Region,第一次混合回收回收掉了20個Region,接着恢復系統運行一下子,而後再執行一次"混合回收",再次回收掉20個Region,如此反覆執行8次混合回收階段,把預約的160個Region都回收掉,並且還把系統停頓時間控制在指定範圍內。
那麼爲何要反覆回收屢次呢?由於中止系統一下子,回收掉一些Region,再讓系統運行一下子,而後再次中止系統一下子,再次回收掉一些Region,這樣能夠儘量讓系統不要停頓時間過長,能夠在屢次回收的間隙,也運行一下。
還有一個參數,"-XX:G1HeapWasterPercent",默認值是5%,意思是說,在混合回收的時候,對Region回收都是基於複製算法進行的,都是把要回收的Region裏的存活對象放入其餘Region,而後這個Region中的垃圾對象所有清理掉。這樣的話在回收過程當中就會不斷空出來新的Region,一旦空閒出來的Region數量達到了堆內存的5%,就會當即中止混合回收,本次垃圾回收結束。從這裏也能看出來G1總體是基於複製算法進行Region垃圾回收的,不會出現內存碎片的問題,不須要像CMS那樣標記-清理以後,再進行內存碎片的整理。
還有一個參數,"-XX:G1MixedGCLiveThresholdPercent",默認值是85%,意思是肯定要回收的Region的時候,必須是存活對象低於85%的Region才能夠進行回收。不然要是一個Region的存活對象多餘85%,你還要回收它幹什麼?這個時候要把85%的對象都拷貝到別的Region,成本是很高的。
若是在進行Mixed GC的時候,不管是年輕代仍是老年代都基於複製算法進行回收,都要把各個Region的存活對象拷貝到別的Region裏去,此時萬一出現拷貝的過程當中發現沒有空閒的Region能夠承載本身的存活對象了,就會觸發一次失敗。一旦失敗,立馬就會切換爲中止系統程序,而後採用單線程進行標記、清理和壓縮整理,空閒出來一批Region,這個過程是極慢極慢的。
當你的系統部署在大內存機器上的時候,好比說你的機器是32核64G的機器,此時你分配給系統的內存有幾十個G,新生代的Eden區可能30G~40G的內存。相似Kafka、Elasticsearch之類的大數據相關係統,都是部署在大內存的機器上的,此時若是你的系統負載很是的高,極可能每秒幾萬的訪問請求到Kafka、Elasticsearch上去,致使Eden區的幾十個G內存頻繁塞滿要觸發垃圾回收,假設1分鐘會塞滿一次。而後每次垃圾回收要停頓掉Kafka、Elasticsearch的運行,而後執行垃圾回收大概須要幾秒鐘,此時你發現,可能每過一分鐘,你的系統就要卡頓幾秒鐘,有的請求一旦卡死幾秒鐘就會超時報錯,致使你的系統頻繁出錯。
用G1垃圾回收器。G1垃圾回收器能夠設置一個指望的每次GC停頓時間,G1基於它的Region內存劃分原理,就能夠在運行一段時間以後,只針對其中一部分的Region進行垃圾回收,騰出來部份內存,接着還能夠繼續讓系統運行。G1天生就適合這種大內存機器的JVM運行,能夠完美解決大內存垃圾回收時間過長的問題。
新生代gc通常問題不會太大,可是真正問題最大的地方,在於頻繁觸發老年代的GC。對象進入老年代的幾個條件:年齡太大了、動態年齡斷定規則、新生代gc後存活對象太多沒法放入Survivor中。下面從新分析一下這幾個條件:第一個,對象年齡太大了,這種對象通常不多,都是系統中確實須要長期存在的核心組件,它們通常不須要被回收掉,因此在新生代熬過默認15次垃圾回收以後就會進入老年代。
第二個,動態年齡斷定規則,若是一次新生代gc事後,發現Survivor區域中的幾個年齡的對象加起來超過了Survivor區域的50%,好比說年齡1+年齡2+年齡3的對象大小總和,超過了Survivor區域的50%,此時就會把年齡3以上的對象都放入老年代。
第三個,新生代垃圾回收事後,存活對象太多了,沒法放入Survivor中,此時直接進入老年代。
其實在上述條件中,第二個和第三個都是很關鍵的,一般若是你的新生代中的Survivor區域內存太小,就會致使上述第二個和第三個條件頻繁發生,而後致使大量對象快速進入老年代,進而頻繁觸發老年代的gc。
老年代gc一般來講都很耗費時間,不管是CMS垃圾回收器仍是G1垃圾回收器,由於好比說CMS就要經歷初始標記、併發標記、從新標記、併發清理、碎片整理幾個環節,過程很是的複雜,G1一樣也是如此。一般來講,老年代gc至少比新生代gc慢10倍以上,好比新生代gc每次耗費200ms,其實對用戶影響不大,可是老年代每次gc耗費2s,那可能就會致使老年代gc的時候用戶發現頁面上卡頓2s,影響就很大了。
因此一旦你由於jvm內存分配不合理,致使頻繁進行老年代gc,好比幾分鐘就有一次老年代gc,每次gc系統都停頓幾秒鐘,那簡直對你的系統就是致命的打擊。此時用戶會發現頁面上或者APP上常常性的出現點擊按鈕以後卡頓幾秒鐘。
系統真正最大的問題,就是由於內存分配、參數設置不合理,致使你的對象頻繁的進入老年代,而後頻繁觸發老年代gc,致使系統頻繁的每隔幾分鐘就要卡死幾秒鐘。這就是所謂JVM的性能問題,也是JVM性能優化須要優化的東西。
(1)發生Young GC以前進行檢查,若是"老年代可用的連續內存空間"<"新生代歷次Young GC後升入老年代的對象總和的平均大小",說明可能本次Young GC後升入老年代的對象大小,超過了老年代當前可用內存空間。此時必須先觸發一次Old GC給老年代騰出更多的空間,而後再執行Young GC。(2)執行Young GC以後有一批對象須要放入老年代,此時老年代沒有足夠的內存空間存放這些對象了,此時必須當即觸發一次Old GC。
(3)老年代內存使用率超過了92%,也要直接觸發Old GC,固然這個比例是能夠經過參數調整的。
綜上所述,總結成一句話,就是老年代空間不夠了,無法放入更多對象了,這個時候必須執行Old GC對老年代進行垃圾回收。
Old GC執行的時候通常都會帶上一次Young GC,通常Old GC極可能就是在Young GC以前觸發或者在Young GC以後觸發的,因此天然Old GC通常都會跟一次Young GC連帶關聯在一塊兒。另一點,在不少JVM的實現機制裏,其實在老年代達到GC條件的時候,它觸發的實際上就是Full GC,這個Full GC會包含Young GC、Old GC和永久代的GC,因此不少時候咱們籠統的歸納爲當上述條件知足時就會觸發Full GC。
平時咱們對運行中的系統,若是要檢查它的JVM總體運行狀況,比較經常使用的工具之一就是jstat,它能夠輕易的讓你看到當前運行中的系統,它的JVM內的Eden、Survivor、老年代的內存使用狀況,還有Young GC和Full GC的執行次數以及耗時。經過這些指標咱們能夠輕鬆的分析出當前系統的運行狀況,判斷當前系統的內存使用壓力以及GC壓力,還有就是內存分配是否合理。
針對咱們的Java進程執行:jstat -gc PID,就能夠看到這個Java進程(其實本質就是一個JVM)的內存和GC狀況了,最完整、最經常使用、最實用仍是jstat -gc命令。運行這個命令以後會看到以下列:S0C:這是From Survivor的大小
S1C:這是To Survivor區的大小
S0U:這是From Survivor區當前使用的內存大小
S1U:這是To Survivor區當前使用的內存大小
EC:這是Eden區的大小
EU:這是Eden區當前使用的內存大小
OC:這是老年代的大小
OU:這是老年代當前使用的內存大小
MC:這是方法區(元數據區)的大小
MU:這是方法區(元數據區)的當前使用的內存大小
YGC:這是系統運行迄今爲止的Young GC次數
YGCT:這是Young GC的耗時
FGC:這是系統運行迄今爲止的Full GC次數
FGCT:這是Full GC的耗時
GCT:這是全部GC的總耗時
咱們分析線上的JVM進程,最想要知道的信息有哪些?包括以下:
新生代對象增加的速率,Young GC的觸發頻率,Young GC的耗時,每次Young GC後有多少對象是存活下來的,每次Young GC事後有多少對象進入了老年代,老年代對象增加的速率,Full GC的增加頻率,Full GC的耗時。
只要知道了這些信息,結合不一樣的垃圾回收器優化參數,合理分配內存空間,儘量讓對象留在年輕代不進入老年代,避免發生頻繁的Full GC。這就是對JVM最好的性能優化了!
咱們平時對jvm第一個要了解的事兒,就是隨着系統運行,每秒鐘會在年輕代的Eden區分配多少對象。要分析這東西,你只要在線上linux機器上運行以下命令:jstat -gc PID 1000 10。這行命令,它的意思是每隔1秒鐘更新出來最新的一行jstat統計信息,一共執行10次jstat統計。經過這個命令,你能夠很是靈活的對線上機器經過固定頻率輸出統計信息,觀察每隔一段時間的jvm中的Eden區對象佔用變化。舉個例子,執行這個命令以後,第一秒先顯示出來Eden區使用了200MB內存,第二秒顯示出來的那行統計信息裏,發現Eden區使用了205MB內存,第三秒顯示出來的那行統計信息裏,發現Eden區使用了209MB內存,以此類推。此時你能夠輕易的推斷出來,這個系統大概每秒鐘會新增5MB左右的對象。並且這裏能夠根據本身系統的狀況靈活多變的使用,好比你的系統負載很低,不必定每秒都有請求,那麼能夠把上面的1秒鐘調整爲1分鐘,甚至10分鐘,去看大家系統每隔1分鐘或者10分鐘大概增加多少對象。
還有就是通常系統都有高峯和平常兩種狀態,好比系統高峯期用的人不少,此時你就應該在系統高峯期去用上述命令看看高峯期的對象增加速率,而後再在非高峯的平常時間段內看看對象的增加速率。按照上述思路,基本上你能夠對線上系統的高峯和平常兩個時間段內的對象增加速率有很清晰的瞭解。
接着下一步咱們就想知道大概多久會觸發一次Young GC,以及每次Young GC的耗時了。其實多久觸發一次Young GC很容易推測出來,由於系統高峯和平常時候的對象增加速率你都知道了,那麼很是簡單就能夠推測出來高峯期多久發生一次Young GC,平常期多久發生一次Young GC。好比你的Eden區有800MB內存,那麼發現高峯期每秒新增5MB對象,大概高峯期就是3分鐘會觸發一次Young GC。平常期每秒新增0.5MB對象,那麼平常期大概須要半個小時纔會觸發一次Young GC。
那麼每次Young GC的平均耗時呢?
簡單,jstat會告訴你迄今爲止系統已經發生了多少次Young GC以及這些Young GC的總耗時。好比系統運行24小時後共發生了260次Young GC,總耗時爲20s,那麼平均下來每次Young GC大概就耗時幾十毫秒的時間,你大概就知道每次Young GC的時候會致使系統停頓幾十毫秒。
接着咱們想要知道,每次Young GC後有多少對象會存活下來,以及有多少對象會進入老年代。其實每次Young GC事後有多少對象會存活下來,這個無法直接看出來,可是有辦法能夠大體推測出來。以前咱們已經推算出來高峯期的時候多久發生一次Young GC,好比3分鐘會有一次Young GC,那麼此時咱們能夠執行下述jstat命令:jstat -gc PID 180000 10。這就至關因而讓它每隔三分鐘執行一次統計,連續執行10次。此時能夠觀察一下,每隔三分鐘以後發生了一次Young GC,Eden、Survivor、老年代的對象變化。
正常來講,Eden區確定會在幾乎放滿以後從新變得裏面對象不多,好比800MB的空間就使用了幾十MB。Survivor區確定會放入一些存活對象,老年代可能會增加一些對象佔用。因此這裏的關鍵,就是觀察老年代的對象增加速率。
從一個正常的角度來看,老年代的對象是不太可能不停的快速增加的,由於普通的系統其實沒那麼多長期存活的對象。若是你發現每次Young GC事後,老年代對象都要增加幾十MB,那頗有可能就是你一次Young GC事後存活對象太多了。
存活對象太多,可能致使放入Survivor區域以後觸發了動態年齡斷定規則進入老年代,也多是Survivor區域放不下了,因此大部分存活對象進入老年代,最多見的就是這兩種狀況。若是你的老年代每次在Young GC事後就新增幾百KB,或者幾MB的對象,這個還算情有可原,可是若是老年代對象快速增加,那必定是不正常的。因此經過上述觀察策略,你就能夠知道每次Young GC事後多少對象是存活的,實際上Survivor區域裏的和進入老年代的對象,都是存活的。
你也就能夠知道老年代對象的增加速率了,好比每隔3分鐘一次Young GC,每次會有50MB對象進入老年代,這就是老年代對象的增加速率,每隔3分鐘增加50MB。
只要知道了老年代對象的增加速率,那麼Full GC的觸發時機就很清晰了,好比老年代總共有800MB的內存,每隔3分鐘新增50MB對象,那麼大概每小時就會觸發一次Full GC。而後能夠看到jstat打印出來的系統運行迄今爲止的Full GC次數以及總耗時,好比一共執行了10次Full GC,共耗時30s,每次Full GC大概就是須要耗費3s左右。
若是單單只是要了解JVM的運行情況,而後去進行JVM GC優化,一般來講jstat就徹底夠用了。可是有的時候可能咱們會發現JVM新增對象的速度很快,而後就想要去看看,到底什麼對象佔據了那麼多的內存。若是發現有的對象在代碼中能夠優化一下建立的時機,避免那種對象對內存佔用過大,那麼也許甚至能夠去反過來優化一下代碼。固然,其實若是不是出現OOM那種極端狀況,也並無那麼大的必要去着急優化代碼。
jmap -histo PID這個命令會打印當前jvm中的對象對內存佔用的狀況,讓你能夠快速瞭解當前內存裏究竟是哪一個對象佔用了大量的內存空間。它會按照各類對象佔用內存空間的大小降序排列,把佔用內存最多的對象放在最上面。
jmap -dump:live,format=b,file=dump.hprof PID這個命令會在當前目錄下生成一個dump.hprof文件,這裏是二進制的格式,是不能直接打開看的,它會把這一時刻JVM堆內存裏全部對象的快照放到文件裏去,供你後續去分析。
jhat dump.hprof -port 7000接着就可使用jhat去分析堆快照了,jhat內置了web服務器,它支持你經過瀏覽器以圖形化的方式分析堆轉儲快照,能夠指定本身想要的http端口號,默認是7000端口。接着你就能夠在瀏覽器上訪問當前這臺機器的7000端口,經過圖形化的方式去分析堆內存裏的對象分佈狀況了。
你們平時在開發一個新系統的時候,通常完成開發以後,要經歷測試以及上線的過程。在系統開發完畢以後,實際上能夠對系統進行預估性的優化。那什麼叫作預估性的優化呢?就是本身估算系統每秒大概多少請求,沒個請求會建立多少對象,佔用多少內存,機器應該選用什麼樣的配置,年輕代應該給多少內存,Young GC觸發的頻率,對象進入老年代的速率,老年代應該給多少內存,Full GC觸發的頻率。這些東西實際上是能夠根據你本身寫的代碼,大體合理的預估一下的。在預估完成以後,就能夠結合各類優化思路,先給本身的系統設置一些初始化的JVM參數,好比堆內存大小,年輕代大小,Eden和Survivor的比例,老年代的大小,大對象的閥值,大對象進入老年代的閥值,等等。
優化思路其實簡單來講就一句話:儘可能讓每次Young GC後的存活對象小於Survivor區域的50%,都留存在年輕代裏。儘可能別讓對象進入老年代。儘可能減小Full GC的頻率,避免頻繁Full GC對JVM性能的影響。
一般一個新系統開發完畢以後,就會通過一連串的測試。從本地的單元測試,到系統集成測試,再到測試環境的功能測試,預發佈環境的壓力測試,要保證系統的功能所有正常,並且在必定壓力下性能、穩定性和併發能力都正常,最後纔會部署到生產環境運行。這裏很是關鍵的一個環節就是預發佈環境的壓力測試,一般在這個環節,會使用一些壓力測試工具模擬好比1000個用戶同時訪問系統形成每秒500個請求的壓力,而後看系統可否支撐柱每秒500請求的壓力。同時看系統各個接口的響應延時是否在好比200ms以內,也就是接口性能不能太慢,或者是在數據庫中模擬出來百萬級單表數據,而後看系統是否還能穩定運行。不少開源的壓力測試工具均可以輕鬆模擬出N個用戶同時訪問你係統的場景,還能給你一份壓力測試報告,告訴你係統能夠支撐每秒多少請求,包括系統接口的響應延時。在這個環節,一般壓測工具會對系統發起持續不斷的請求,持續很長時間,好比幾個小時,甚至幾天時間。
因此此時,你們徹底就能夠在這個環節,對測試機器運行的系統,採用jstat工具來分析在模擬真實環境的壓力下,JVM的總體運行狀態。具體如何使用jstat來進行分析,以前都講的很詳細了,包括如何藉助jstat的各類功能分析出來如下JVM的關鍵運行指標:新生代對象增加的速率,Young GC的觸發頻率,Young GC的耗時,每次Young GC後有多少是存活下來的,每次Young GC事後有多少對象進入了老年代,老年代對象增加的速率,Full GC的觸發頻率,Full GC的耗時。
而後根據壓測環境中的JVM運行情況,若是發現對象過快進入老年代,多是由於年輕代過小致使頻繁Young GC,而後Young GC的時候不少對象仍是存活的,結果Survivor也過小,致使不少對象頻繁進入老年代。固然也多是別的什麼緣由。此時就須要結合各類優化思路,合理調整新生代、老年代、Eden、Survivor各個區域的內存大小,保證對象儘可能留在年輕代,不要過快進入老年代中。
不要去網上胡亂搜索JVM優化的博客,看到裏面人家怎麼優化,你就怎麼優化,好比不少博客說年輕代和老年代的佔比通常是3:8,其實徹底是片面的。每一個系統都是不同的,特色不一樣,複雜度不一樣。記住一點:真正的優化,必須是你根據本身的系統,實際觀察以後,而後合理調整內存分佈,根本沒什麼固定的JVM優化模板。當你對壓測環境下的系統優化好JVM參數以後,觀察Young GC和Full GC頻率都很低,此時就能夠部署系統上線了。
當你的系統上線以後,你就須要對線上系統的JVM進行監控,這個監控一般來講有兩種辦法。第一種方法會low一點,其實就是天天在高峯期和低峯期都用jstat、jmap、jhat等工具去看看線上系統的JVM運行是否正常,有沒有頻繁Full GC的問題。若是有就優化,沒有的話,平時天天都定時去看看,或者每週都去看看便可。
第二種方法在中大型公司裏會多一些,不少中大型公司都會部署專門的監控系統,比較常見的有Zabbix、OpenFalcon、Ganglia,等等。而後你部署的系統均可以把JVM統計項發送到這些監控系統裏去。此時你就能夠在這些監控系統可視化的界面裏,看到你須要的全部指標,包括你的各個內存區域的對象佔用變化曲線,直接能夠看到Eden區的對象增速,還會告訴你Young GC發生的頻率以及耗時,包括老年代的對象增速以及Full GC的頻率和耗時。並且這些工具還容許你設置監控。也就是說,你能夠指定一個監控規則,好比線上系統的JVM,若是10分鐘以內發生5次以上Full GC,就須要發送報警給你。好比發生到你的郵箱、短信或釘釘裏,這樣你就不用本身天天去看看了。
簡單一句話總結:對線上運行的系統,要否則用命令行工具手動監控,發現問題就優化,要否則就是依託公司的監控系統進行自動監控,可視化查看平常系統的運行狀態。
正常狀況下的系統,會有必定頻率的Young GC,通常在幾分鐘一次Young GC,或者幾十分鐘一次Young GC,一次耗時在幾毫秒到幾十毫秒的樣子,都是正常的。正常的Full GC頻率在幾十分鐘一次,或者幾個小時一次,這個範圍內都是正常的,一次耗時應該在幾百毫秒的樣子。
因此你們若是觀察本身線上系統就是這個性能表現,基本上問題都不太大。固然,實際線上系統不少時候會遇到一些JVM性能問題,就是Full GC過於頻繁,每次還耗時不少的狀況,此時就須要一些優化了。
一旦系統發生頻繁Full GC,大概看到的一些表象以下:機器CPU負載太高;
頻繁Full GC報警;
系統沒法處理請求或者處理過慢;
因此一旦發生上述幾個狀況,你們第一時間得想到是否是發生了頻繁Full GC。
第一種,系統承載高併發請求,或者處理數據量過大,致使Young GC很頻繁,並且每次Young GC事後存活對象太多,內存分配不合理,Survivor區域太小,致使對象頻繁進入老年代,頻繁觸發Full GC。第二種,系統一次性加載過多數據進內存,搞出來不少大對象,致使頻繁有大對象進入老年代,必然頻繁觸發Full GC。
第三種,系統發生了內存泄漏,莫名其妙建立了大量的對象,始終沒法回收,一直佔用在老年代裏,必然頻繁觸發Full GC。
第四種,MetaSpace(永久代)由於加載類過多觸發Full GC。
第五種,誤調用System.gc()觸發Full GC。
其實常見的頻繁Full GC緣由無非就上述那幾種,因此你們在線上處理Full GC的時候,就從這幾個角度入手去分析便可,核心利器就是jstat。
若是jstat分析發現Full GC緣由是第一種,那麼就合理分配內存,調大Survivor區域便可。
若是jstat分析發現是第二種或第三種緣由,也就是老年代一直有大量對象沒法回收掉,年輕代升入老年代的對象並很少,那麼就dump出來內存快照,而後用MAT工具進行分析便可。經過分析,找出來什麼對象佔用內存過多,而後經過一些對象的引用和線程執行堆棧的分析,找到哪塊代碼弄出來那麼多的對象的,接着優化代碼便可。
經過jstat分析發現內存使用很少,還頻繁觸發Full GC,必然是第四種和第五種,此時對應的進行優化便可。
爲了簡化JVM的參數設置和優化,建議各個公司和團隊leader作一份JVM參數模板出來,設置一些常見參數便可。核心就是一些內存區域的分配、垃圾回收器的指定、CMS性能優化的一些參數(好比壓縮、併發,等等),常見的一些參數,包括禁止System.gc(),打印出來gc日誌,等等。
由於各類各樣的狀況,一旦出現了高併發場景,致使ygc後不少請求還沒處理完畢,存活對象太多,可能就在Survivor區域放不下了,此時就只能進入到老年代裏去了,老年代很快就會放滿,一旦老年代放滿了就會觸發Full GC。咱們假設ygc事後有一批存活對象,Survivor放不下,此時就等着要進入老年代裏,然而老年代也滿了,那麼就得等着老年代進行CMS GC回收掉一些對象,才能讓年輕代裏存活下來的對象放進去,可是這時不幸的事情發生了,老年代GC事後依然存活下來了不少的對象,沒有足夠的剩餘空間來存放年輕代中的存活對象。這時候會發生什麼?那就是內存溢出了!由於老年代都已經塞滿了,你還要往裏面放東西,並且觸發了Full GC回收了老年代仍是沒有足夠內存空間,你堅持要放?那隻能給你一個內存溢出的異常了!JVM跑不動了,崩潰掉。這就是堆內存實在放不下過多對象致使內存溢出的典型範例。
發生堆內存溢出的緣由其實總結下來,就一句話:有限的內存中放了過多的對象,並且大多數都是存活的,此時即便GC事後仍是大部分都存活,因此要繼續放入更多對象已經不可能了,只能引起內存溢出問題。
因此通常來講發生內存溢出有兩種主要的場景:
系統承載高併發請求,由於請求量過大,致使大量對象都是存活的,因此要繼續放入新的對象實在是不行了,此時就會引起OOM系統崩潰。
系統有內存泄漏的問題,就是莫名其妙弄了不少的對象,結果對象都是存活的,沒有及時取消對他們的引用,致使觸發GC仍是沒法回收,此時只能引起內存溢出,由於內存實在放不下更多對象了。
所以總結起來,通常引起OOM,要否則是系統負載太高,要否則就是有內存泄漏的問題。
這個OOM問題,一旦你的代碼寫的不太好,或者設計有缺陷,仍是比較容易引起的。