JVM:垃圾收集器与内存分配策略
垃圾收集器與內(nèi)存分配策略
1、對象已死嗎
1)、引用計數(shù)算法
引用計數(shù)算法:給對象中添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值就加1;當引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器為0的對象就是不可能再被使用的
主流的Java虛擬機里面沒有選用引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。兩個對象相互引用著對方,導致它們的引用計數(shù)都不為0,于是引用計數(shù)算法無法通知GC收集器回收它們
2)、可達性分析算法
可達性分析算法:通過一系列的稱為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
3)、再談引用
Java將引用分為強引用、軟引用、弱引用、虛引用
- 強引用是指類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象
- 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK1.2之后,提供了SoftReference類來實現(xiàn)軟引用
- 弱引用也是用來描述非必需對象的,但是它的強度要比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK1.2之后,提供了WeakReference類來實現(xiàn)弱引用
- 虛引用是最弱的一種引用關(guān)系,一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK1.2之后,提供了PhantomReference類來實現(xiàn)虛引用
4)、生存還是死亡
如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都是為沒有必要執(zhí)行
如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的執(zhí)行是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán)將很可能會導致F-Queue隊列中其他對象永久處于等待,甚至導致整個內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標記,如果對象在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象關(guān)聯(lián)即可,那在第二次標記時他將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了
任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執(zhí)行
finalize()方法的運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序,因此不建議使用
5)、回收方法區(qū)
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類
以常量池中字面量的回收為例,假如一個字符串“abc”已經(jīng)進入了常量池中,沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收,而且必要的話,這個“abc”常量就會被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似
類需要滿足下面3個條件才能算是無用的類
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例
- 加載該類的ClassLoader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法
2、垃圾收集算法
1)、標記-清除算法
標記-清除算法分為“標記”和“清除”兩個階段:首先標記處所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象
不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的空間碎片,空間碎片太多可能導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作
2)、復制算法
復制算法:將可用容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可
復制算法的代價是將內(nèi)存縮小為了原來的一半,未免太高了一點
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor空間。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,當Survivor空間不夠用時,需要依賴其他內(nèi)存(老年代)進行分配擔保
3)、標記-整理算法
標記-整理算法標記過程仍然與標記-清除算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存
4)、分代收集算法
當前商業(yè)虛擬機的垃圾收集都采用“分代收集”算法,根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴āT谛律?#xff0c;每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復制算法;而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,那就必須使用標記-清理或者標記-整理算法來進行回收
3、HotSpot的算法實現(xiàn)
1)、枚舉根節(jié)點
從可達性分析中從GC Roots節(jié)點找引用鏈這個操作為例,可作為GC Roots的節(jié)點主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間
可達性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這里“一致性”的意思是指在整個分析期間整個執(zhí)行系統(tǒng)看起來就像被凍結(jié)在某個時間點上,不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,該點不滿足的話分析結(jié)果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執(zhí)行線程(Sun將這件事情稱為“Stop The World”)的其中一個重要原因,即使是在號稱幾乎不會發(fā)生停頓的CMS收集器中,枚舉根節(jié)點時也是必須要停頓的
由于目前的主流Java虛擬機使用的都是準確式GC(虛擬機可以知道內(nèi)存中某個位置的數(shù)據(jù)具體是什么類型),所以當執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機應(yīng)當是有辦法直接得知哪些地方存放著對象引用。在HotSpot的實現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達到這個目的的,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,GC在掃描時就可以直接得知這些信息了
2)、安全點
在OopMap的協(xié)助下,HotSpot可以快速且準確地完成GC Roots枚舉,OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高
HotSpot也的確沒有為每條指令都生成OopMap,只是在特定的位置記錄了這些信息,這些位置稱為安全點,即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定基本上是以程序是否具有讓程序長時間執(zhí)行的特征為標準進行選定的——因為每條指令執(zhí)行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,長時間執(zhí)行的最明顯特征就是指令序列復用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會產(chǎn)生Safepoint
對于Safepoint,另一個需要考慮的問題是如何在GC發(fā)生時讓所有線程都跑到最近的安全點上再停頓下來。有兩種方法:搶先式中斷和主動式中斷
搶先式中斷:搶先式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。現(xiàn)在幾乎沒有虛擬機實現(xiàn)采用搶先式中斷來暫停線程從而響應(yīng)GC事件
主動式中斷:主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設(shè)置一個標志,各個線程執(zhí)行時主動去輪詢這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起,輪詢標志的地方和安全點是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方
3)、安全區(qū)域
Safepoint機制保證了程序執(zhí)行時,在不太長的時間內(nèi)就會遇到可進入GC的Safepoint,但線程處于Sleep狀態(tài)或者Blocked狀態(tài),這時候線程無法響應(yīng)JVM的中斷請求
安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任意地方開始GC都是安全的
在線程執(zhí)行到Safe Region中的代碼時,首先標識自己已經(jīng)進入了Safe Region,那樣,當在這段時間里JVM要發(fā)起GC時,就不用管標識自己為Safe Region狀態(tài)的線程了。在線程要離開Safe Region時,它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點枚舉(或者整個GC過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開Safe Region的信號為止
4、垃圾收集器
如果兩個收集器之間存在連線,就說明它們可以搭配使用
1)、Serial收集器
Serial收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)是虛擬機新生代收集的唯一選擇,是一個單線程的收集器,但它的單線程的意義并不僅僅說明它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結(jié)束
2)、ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作
并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)
并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行),用戶程序在繼續(xù)運行,而垃圾收集程序運行在另一個CPU上
3)、Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器
Parallel Scavenge收集器的目標是達到一個可控制的吞吐量,所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
停頓時間越短越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗,而高吞吐量則可以高效地利用CPU時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)
Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)
MaxGCPauseMillis參數(shù)允許的值是一個大于0的毫秒數(shù),收集器盡可能地保證內(nèi)存回收花費的時間不超過設(shè)定值。不過不要認為把這個參數(shù)的值設(shè)置得稍微小一點就能使得系統(tǒng)的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統(tǒng)把新生代調(diào)小一些,收集300MB新生代肯定要比收集500MB快,這也直接導致垃圾收集發(fā)生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現(xiàn)在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了
GCTimeRatio參數(shù)的值應(yīng)當是一個大于0且小于100的整數(shù),也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)置為19,那允許的最大GC時間就占總時間的5%(即1/(1+19)),默認值為99,那就允許最大1%的垃圾收集時間
Parallel Scavenge收集器也經(jīng)常稱為吞吐量優(yōu)先收集器
Parallel Scavenge收集器還有一個開關(guān)參數(shù)-XX:UseAdaptiveSizePolicy,當這個參數(shù)打開之后,就不需要手動指定新生代的大小、Eden與Survivor區(qū)的比例、晉升老年代對象大小等細節(jié)參數(shù)了,虛擬機會根據(jù)當前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)的調(diào)節(jié)策略
4)、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法,主要意義是在于給Client模式下的虛擬機使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后備預案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用
5)、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標記整理算法
在注重吞吐量以及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器
6)、CMS收集器
CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,基于標記-清除算法
整個過程分為4個步驟:
- 初始標記
- 并發(fā)標記
- 重新標記
- 并發(fā)清除
初始標記、重新標記這兩個步驟仍然需要“Stop The World”,初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快,并發(fā)標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短
由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起工作,所以,總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的
CMS收集器有3個明顯的缺點:
A.CMS收集器對CPU資源非常敏感,在并發(fā)階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應(yīng)用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當CPU在4個以上時,并發(fā)回收時垃圾收集線程不少于25%的CPU資源,并且隨著CPU數(shù)量的增加而下降。但是當CPU不足4個時,CMS對用戶程序的影響就可能變得很大
虛擬機提供了一種稱為增量式并發(fā)收集器的CMS收集器變種,在并發(fā)標記、清理的時候讓GC線程、用戶線程交替運行,盡量減少GC線程的獨占資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,也就是速度下降沒有那么明顯。實踐證明,增量式的CMS收集器效果很一般,在JDK1.7中已經(jīng)被聲明為deprecated,即不再提倡用戶使用
B.CMS收集器無法處理浮動垃圾,可能出現(xiàn)Concurrent Mode Failure失敗而導致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為浮動垃圾。由于在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發(fā)收集時的程序運作使用。在JDK1.5的默認設(shè)置下,CMS收集器當老年代使用了68%的空間后被會被激活,如果在應(yīng)用中老年代增長不是太快,可以適當調(diào)高參數(shù)-XX:CMSInitiatingOccupancyFraction的值來提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)從而獲取更好的性能,在JDK1.6中,CMS收集器的啟動閾值已經(jīng)提升至92%。要是CMS運行期間預留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次Concurrent Mode Failure失敗,這時虛擬機將啟動后備預案:臨時啟動Serial Old收集器來重新進行老年代的垃圾收集,所以說參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置得太高很容易導致大量Concurrent Mode Failure失敗,性能反而降低
C.CMS是一款基于標記-清除算法實現(xiàn)的收集器,收集結(jié)束時會有大量空間碎片產(chǎn)生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現(xiàn)老年代還有很大空間剩余,但是無法找到足夠大的連續(xù)空間來分配當前對象,不得不提前觸發(fā)一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關(guān)參數(shù)(默認就是開啟的),用于在CMS收集器頂不住要進行Full GC時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無法并發(fā)的,空間碎片問題沒有了,但停頓時間不得不變長。還提供了另外一個參數(shù)-XX:+CMSFullGCBeforeCompaction,這個參數(shù)是用于設(shè)置執(zhí)行多少次不壓縮的Full GC后,跟著來一次帶壓縮的(默認值是0,表示每次進入Full GC時都進行碎片整理)
7)、G1收集器
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器
G1具備以下特點:
- 并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU來縮短Stop The World停頓的時間,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行
- 分代收集
- 空間整合:G1從整體上看是基于標記-整理算法實現(xiàn)的收集器,從局部(兩個Region之間)上看是基于復制算法實現(xiàn)的,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存
- 可預測的停頓:G1能建立可預測的停頓時間模型,能讓使用者明確指定一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經(jīng)是實時Java的垃圾收集器的特征了
G1將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲取的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率
在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應(yīng)的Remembered Set,虛擬機發(fā)現(xiàn)程序在對Reference類型的數(shù)據(jù)進行寫操作時,會產(chǎn)生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中,如果是,便通過CardTable把相關(guān)引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內(nèi)存回收時,在GC根節(jié)點的枚舉范圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏
G1的運作大致劃分為幾個步驟:
- 初始標記
- 并發(fā)標記
- 最終標記
- 篩選回收
初始標記階段僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS的值,讓下一階段用戶程序并發(fā)運行時,能在正常可用的Region中創(chuàng)建新對象,這階段需要停頓線程,但耗時很短。并發(fā)標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。而最終標記階段則是為了修改正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄,虛擬機將這段時間對象記錄在線程Remebered Set Logs里面,最終標記階段需要把Remebered Set Logs的數(shù)據(jù)合并到Remebered Set中,這階段需要停頓線程,但是可并行執(zhí)行。最后在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率
8)、理解GC日志
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs] 100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]最前面的數(shù)字“33.125:”和“100.667:”代表了GC發(fā)生的時間,這個數(shù)字的含義是從Java虛擬機啟動以來經(jīng)過的秒數(shù)
GC日志開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區(qū)分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發(fā)生了Stop The World的
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發(fā)生的區(qū)域,這里顯示的區(qū)域名稱與使用的GC收集是密切相關(guān)的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。
如果是ParNew收集器,新生代名稱就會變?yōu)椤癧ParNew”,意為“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代名為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的
后面方括號內(nèi)部的“3324K->152K(3712K)”含義是“GC前該內(nèi)存區(qū)域已使用容量->GC后該內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘?#xff09;”。 而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總?cè)萘?#xff09;”
補充:
A.在JDK1.8中,Parallel Scavenge收集器新生代名為“PSYoungGen”,SerialGC收集器新生代名為"def new generation",ParNew收集器新生代名為“par new generation”
B.JDK默認垃圾收集器
JDK1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
JDK1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
JDK1.9 默認垃圾收集器G1
5、內(nèi)存分配與回收策略
1)、對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配。當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor GC
虛擬機提供了-XX:+PrintGCDetails這個收集器日志參數(shù),告訴虛擬機在發(fā)生垃圾收集行為時打印內(nèi)存回收日志,并在進程退出的時候輸出當前的內(nèi)存各區(qū)域分配情況
補充:
A.-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8幾個參數(shù)的含義
在運行時通過-Xms20M、-Xmx20M、-Xmn10M這3個參數(shù)限制了Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例是8:1,“eden space 8192K、from space 1024K、to space 1024K”,新生代總可用空間為9216KB(Eden區(qū)+1個Survivor區(qū)的總?cè)萘?#xff09;
B.Minor GC和Full GC有什么不一樣?
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快
- 老年代GC(Full GC):指發(fā)生在老年代的GC,出現(xiàn)了 Full GC經(jīng)常會伴隨至少一次的Minor GC。Full GC的速度一般會比Minor GC慢10倍以上
2)、大對象直接進入老年代
大對象指需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組
虛擬機提供了一個-XX:PretenureSizeThreshold參數(shù),令大于這個設(shè)置值的對象直接在老年代分配。這樣做的目的是避免在Eden區(qū)及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復制
PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數(shù),Parallel Scavenge收集器一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場合,可以考慮ParNew加CMS的收集器組合
3)、長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡計數(shù)器,如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設(shè)為1.對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(15歲),就將會被晉升到老年代中
4)、動態(tài)對象年齡判定
虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
5)、空間分配擔保
在發(fā)生Minor GC之前,虛擬機會檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會檢查HandlePromotionFailure設(shè)置值是否允許擔保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,HandlePromotionFailure設(shè)置不允許冒險,那這時也要改為進行一次Full GC
新生代使用復制收集算法,但是為了內(nèi)存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現(xiàn)大量對象在Minor GC后仍然存活的情況,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內(nèi)存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間
取平均值進行比較其實仍然是一種動態(tài)概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠遠高于平均值的話,依然會導致?lián)J H绻霈F(xiàn)了HandlePromotionFailure失敗,那就只好在失敗后重新發(fā)起一次Full GC
總結(jié)
以上是生活随笔為你收集整理的JVM:垃圾收集器与内存分配策略的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从来也科技首次入选Gartner RPA
- 下一篇: 自己的应用跳转到应用宝评分界面