⑴ GC的復制演算法和標記整理演算法
復制演算法
復制演算法是將內存劃分為兩個區間,在任意時間點,所有動態分配的對象都只能分配在其中的一個區間(稱為活動區間),而另外一個區間(空閑區間)是空閑的。
當有效內存空間耗盡時,JVM將暫停程序運行,開啟復制GC線程。接下來GC線程會將活動區的存活對象,全部復制到空閑區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。
此時,空閑區間已經與活動區間交換,而垃圾對象現在已經全部留在了原來的活動區間,也就是現在的空閑區間。事實上,在活動區間轉換為空閑區間的同時,垃圾對象已經被一次性全部回收了。
開始時:
在被GC線程處理後:
可以看到,1和4號對象被清楚了,而2,3,5,6號對象則是規則的排列在剛才的空閑區間,也就是現在的活動區間之間,此時左半部分已經變成空閑區間,然而,如中在下一次GC之後,左半部分再次變成活動區間。
顯然:
復制演算法缺點時非常明顯的:
1.直接浪費了一般的內存。
2.再就是加入對象存活率非常高,達到了極端的100%,那麼我們需要將所有的對象都復制一遍,並將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定的程度時,將會變得不可忽視。
由此可見,復制演算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們態悔必須要克服50%內存浪費。
標記整理演算法
分為兩個階段:
1.標記:遍歷GC roots,然後將存活的對象標記。
2.整理:移動所帆橡正有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收,這個階段才為整理階段。
它與GC 前後與復制演算法類似,只不過沒有了活動區間和空閑區間
倘若此時GC線程開始工作,那麼緊接著開始的就是標記階段了。此階段與標記/清除演算法的標記階段是一樣一樣的,我們看標記階段過後對象的狀態,如下圖。
接下來,便應該是整理階段了。我們來看當整理階段處理完以後,內存的布局是如何的,如下圖。
⑵ 深入理解GC垃圾回收機制
在我們程序運行中會不斷創建新的對象,這些對象會存儲在內存中,如果沒有一套機制來回收這些內存,那麼被佔用的內存會越來越多,可用內存會越來越少,直至內存被消耗完。於是就有了一套垃圾回收機制來做這件維持系統平衡的任務。
1.確保被引用對象的內存不被錯誤的回收
2.回收不再被引用的對象的內存空間
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時, 計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
優點:引用計數收集器可以很快地執行,交織在程序的運行之中。
缺點:很難處理循環引用,比如上圖中相互引用的兩個對象,計數器不為0,則無法釋放,但是這樣的對象存在是沒有意義的,空占內存了。
引用計數法處理不了的相互引用的問題,那麼就有了可達性分析來解決了這個問題。
從GC Roots作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之最終不能與GC Roots有引用關系的視為不可達,不可達對象即為垃圾回收對象。
我自己的理解是,皇室家族每過一段時間,會進行皇室成員排查,從皇室第一代開始往下找血緣關系的後代,如果你跟第一代皇室沒有關系,那麼你就會被剔除皇室家族。
1.虛擬機棧中引用的對象(正在運行的方法使用到的變數、參數等)
2.方法區中類靜態屬性引用的對象(static關鍵字聲明的欄位)
3.方法區中常量引用的對象,(也就是final關鍵字聲明的欄位)
4.本地方法棧中引用的對象(native方法)
1.顯示地賦予某個對象為null,切斷可達性
在main方法中創建objectA、objectB兩個局部變數,而且相互引用。相互引用直接調System.gc()是回收不了的。而將兩者都置為null,切斷相互引用,切斷了可達性,與GCRoots無引用,那麼這兩個對象就會被回收調。
2.將對象的引用指向另一個對象
這里將one的引用也指向了two引用指向的對象,那麼one原本指向的對象就失去了GCRoots引用,這里就判斷該對象可被回收。
3.局部對象的使用
當方法執行完,局部變數object對象會被判定為可回收對象。
4.只有軟、弱、虛引用與之關聯
new出來的對象被強引用了,就需要去掉強引用,改為弱引用。被弱引用之後,需要置空來幹掉強引用,達到隨時可回收的效果。
只被軟引用的對象在內存不足的情況,可能會被GC回收掉。
只被弱引用持有的對象,隨時都可能被GC回收,該對象就為可回收對象。
是不是被判定為了可回收對象,就一定會被回收了呢。其實Ojbect類中還有一個finalize方法。這個方法是對象在被GC回收之前會被觸發的方法。
該方法翻譯過來就是:當垃圾回收確定不再有對該對象的引用時,由垃圾回收器在對象上調用。子類重寫finalize方法以處置系統資源或執行其他清除。說人話就是對象死前會給你一個迴光返照,讓你清醒一下,想干什麼就干什麼,甚至可以把自己救活。我們可以通過重寫finalize方法,來讓對象復活一下。
示例:
執行的結果:
這里我們重寫FinalizeGC類的finalize方法, 使用FinalizeGC.instance = this語句,讓對象又有了引用,不再被判定為可回收對象,這里就活了。然後再置空再回收一下,這個對象就死了,沒有再被救活了。所以finalize方法只能被執行一次,沒有再次被救活的機會。
在JDK1.8版本廢棄了永久代,替代的是元空間(MetaSpace),元空間與永久代上類似,都是方法區的實現,他們最大區別是:元空間並不在JVM中,而是使用本地內存。
元空間有注意有兩個參數:
MetaspaceSize :初始化元空間大小,控制發生GC閾值
MaxMetaspaceSize : 限制元空間大小上限,防止異常佔用過多物理內存
為什麼移除永久代?
移除永久代原因:為融合HotSpot JVM與JRockit VM(新JVM技術)而做出的改變,因為JRockit沒有永久代。
有了元空間就不再會出現永久代OOM問題了!
1.Generational Collection(分代收集)演算法
分代收集演算法是GC垃圾回收演算法的總綱領。現在主流的java虛擬機的垃圾收集器都採用分代收集演算法。Java 堆區基於分代的概念,分為新生代(Young Generation)和老年代(Tenured Generation),其中新生代再細分為Eden空間、From Survivor空間和To Survivor空間。 (Survivor:倖存者)
分代收集演算法會結合不同的收集演算法來處理不同的空間,因此在學習分代收集演算法之前我們首先要了解Java堆區的空間劃分。Java堆區的空間劃分在Java虛擬機中,各種對象的生命周期會有著較大的差別。因此,應該對不同生命周期的對象採取不同的收集策略,根據生命周期長短將它們分別放到不同的區域,並在不同的區域採用不同的收集演算法,這就是分代的概念。
當執行一次GC Collection時,Eden空間的存活對象會被復制到To Survivor空間,並且之前經過一次GC Collection並在From Survivor空間存活的仍年輕的對象也會復制到To Survivor空間。
對象進入到From和To區之後,對象的GC分代年齡ege的屬性,每經過GC回收存活下來,ege就會+1,當ege達到15了,對象就會晉級到老年代。
2.Mark-Sweep(標記-清除)演算法
標記清除:標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。標記-清除演算法主要是運用在Eden區,該區對象很容易被回收掉,回收率很高。
3.Copying(復制)演算法
復制演算法的使用在From區和To區,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。
缺點:可使用內存縮減為一半大小。
那麼復制演算法使可使用內存大小會減半,設計上是怎麼解決這個問題的呢。就是給From和To區劃分盡可能小的區域。經過大數據統計之後,對象在第一次使用過後,絕大多數都會被回收,所以能進入第一次復制演算法的對象只佔10%。那麼設計上,Eden、From、To區的比例是8:1:1,絕大多數對象會分配到Eden區,這樣就解決了復制演算法縮減可用內存帶來的問題。
4.Mark-Compact (標記—整理)演算法
在新生代中可以使用復制演算法,但是在老年代就不能選擇復制演算法了,因為老年代的對象存活率會較高,這樣會有較多的復制操作,導致效率變低。標記—清除演算法可以應用在老年代中,但是它效率不高,在內存回收後容易產生大量內存碎片。因此就出現了一種標記—整理演算法,與標記—清除演算法不同的是,在標記可回收的對象後將所有存活的對象壓縮到內存的一端,使它們緊湊地排列在一起,然後對邊界以外的內存進行回收,回收後,已用和未用的內存都各自一邊。
垃圾收集演算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現:
Serial 收集器(復制演算法): 新生代單線程收集器,標記和清理都是單線程,
優點是簡單高效;
Serial Old 收集器 (標記-整理演算法): 老年代單線程收集器,Serial 收集器
的老年代版本;
ParNew 收集器 (復制演算法): 新生代收並行集器,實際上是 Serial 收集器
的多線程版本,在多核 CPU 環境下有著比 Serial 更好的表現;
CMS(Concurrent Mark Sweep)收集器(標記-清除演算法): 老年代並行
收集器,以獲取最短回收停頓時間為目標的收集器,具有高並發、低停頓
的特點,追求最短 GC 回收停頓時間。
⑶ 三色標記法與垃圾回收器(CMS、G1)
JVM中的CMS、G1垃圾回收器所使用垃圾回收演算法即為三色標記法。
三色標記法將對象的顏色分為了黑、灰、白,三種顏色。
存在問題:
浮動垃圾:並發標記的過程中,若一個已經被標記成黑色或者灰色的對象,突然變成了垃圾,此時,此對象不是白色的不會被清除,重新標記也不能從GC Root中去找到,所以成為了浮動垃圾,這種情況對系統的影響不大,留給下一次GC進行處理即可。
對象漏標問題(需要的對象被回收):並發標記的過程中,一個業務線程將一個未被掃描過的白色對象斷開引用成為垃圾(刪除引用),同時黑色對象引用了該對象(增加引用)(這兩部可以不分先後順序);因為黑色對象的含義為其屬性都已經被標記過了,重新標記也不會從黑色對象中去找,導致該對象被程序所需要,卻又要被GC回收,此問題會導致系統出現問題,而CMS與G1,兩種回收器在使用三色標記法時,都採取了一些措施來應對這些問題,CMS對增加引用環節進行處理(Increment Update),G1則對刪除引用環節進行處理(SATB)。
在JVM虛擬機中有兩種常見垃圾回收器使用了該演算法:
CMS(Concurrent Mark Sweep)
CMS,是非常有名的JVM垃圾回收器,它起到了承上啟下的作用,開啟了並發回收的篇章。
但是CMS由於許多小問題,現在基本已經被淘汰。
增量更新(Increment Update)
在應對漏標問題時,CMS使用了Increment Update方法來做:
在一個未被標記的對象(白色對象)被重新引用後,==引用它的對象==,若為黑色則要變成灰色,在下次二次標記時讓GC線程繼續標記它的屬性對象。
但是就算時這樣,其仍然是存在漏標的問題:
在一個灰色對象正在被一個GC線程回收時,當它已經被標記過的屬性指向了一個白色對象(垃圾)
而這個對象的屬性對象本身還未全部標記結束,則為灰色不變
而這個GC線程在標記完最後一個屬性後,認為已經將所有的屬性標記結束了,將這個灰色對象標記為黑色,被重新引用的白色對象,無法被標記
補充,CMS除了這個缺陷外,仍然存在兩個個較為致命的缺陷:
解決方案:使用Mark-Sweep-Compact演算法,減少垃圾碎片
當JVM認為內存不夠了,再使用CMS進行並發清理內存可能會發生OOM的問題,而不得不進行Serial Old GC,Serial Old是單線程垃圾回收,效率低
解決方案:降低觸發CMS GC的閾值,讓浮動垃圾不那麼容易占滿老年代
G1(Garbage First)
從G1垃圾回收器開始,G1的物理內存不再分代,而是由一塊一塊的Region組成;邏輯分代仍然存在。
前置知識 — Card Table(多種垃圾回收器均具備)
由於在進行YoungGC時,我們在進行對一個對象是否被引用的過程,需要掃描整個Old區,所以JVM設計了CardTable,將Old區分為一個一個Card,一個Card有多個對象;如果一個Card中的對象有引用指向Young區,則將其標記為Dirty Card,下次需要進行YoungGC時,只需要去掃描Dirty Card即可。
Card Table 在底層數據結構以 Bit Map實現。
CSet(Collection Set)
SATB(Snapshot At The Beginning)
在應對漏標問題時,CMS使用了SATB方法來做:
因為SATB在重新標記環節只需要去重新掃描那些被推到堆棧中的引用,並配合Rset來判斷當前對象是否被引用來進行回收;
並且在最後G1並不會選擇回收所有垃圾對象,而是根據Region的垃圾多少來判斷與預估回收價值(指回收的垃圾與回收的STW時間的一個預估值),將一個或者多個Region放到CSet中,最後將這些Region中的存活對象壓縮並復制到新的Region中,清空原來的Region。
問題:G1會不會進行Full GC?
會,當內存滿了的時候就會進行Full GC;且JDK10之前的Full GC,為單線程的,所以使用G1需要避免Full GC的產生。
解決方案:
加大內存;
提高CPU性能,加快GC回收速度,而對象增加速度趕不上回收速度,則Full GC可以避免;
降低進行Mixed GC觸發的閾值,讓Mixed GC提早發生(默認45%)
G1的第一篇paper(附錄1)發表於2004年,在2012年才在jdk1.7u4中可用。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS。為何oracle要極力推薦G1呢,G1有哪些優點?
首先,G1的設計原則就是簡單可行的性能調優
開發人員僅僅需要聲明以下參數即可:
其中-XX:+UseG1GC為開啟G1垃圾收集器,-Xmx32g 設計堆內存的最大內存為32G,-XX:MaxGCPauseMillis=200設置GC的最大暫停時間為200ms。如果我們需要調優,在內存大小一定的情況下,我們只需要修改最大暫停時間即可。
其次,G1將新生代,老年代的物理空間劃分取消了。
這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。
取而代之的是,G1演算法將堆劃分為若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
PS:在java 8中,持久代也移動到了普通的堆內存空間中,改為元空間。
對象分配策略
說起大對象的分配,我們不得不談談對象的分配策略。它分為3個階段:
對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。
最後,G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。
Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據為空,GC停止工作,應用線程繼續執行。
這時,我們需要考慮一個問題,如果僅僅GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根么?那這樣掃描下來會耗費大量的時間。於是,G1引進了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區內的對象引用。
在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。
但在G1中,並沒有使用point-out,這是由於一個分區太小,分區數量太多,如果是用point-out的話,會造成大量的掃描浪費,有些根本不需要GC的分區引用也掃描了。於是G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當做根來掃描就避免了無效的掃描。由於新生代有多個,那麼我們需要在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。
需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上劃分為固定大小的連續區域,每個區域稱之為卡。卡通常較小,介於128到512位元組之間。Card Table通常為位元組數組,由Card的索引(即數組下標)來標識每個分區的空間地址。默認情況下,每個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記為」0″,即標記為臟被引用,此外RSet也將這個數組下標記錄下來。一般情況下,這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裡面的元素是Card Table的Index。
Young GC 階段:
Mix GC不僅進行正常的新生代垃圾收集,同時也回收部分後台掃描線程標記的老年代分區。
它的GC步驟分2步:
全局並發標記(global concurrent marking)
拷貝存活對象(evacuation)
在進行Mix GC之前,會先進行global concurrent marking(全局並發標記)。 global concurrent marking的執行過程是怎樣的呢?
在G1 GC中,它主要是為Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分為五個步驟:
初始標記(initial mark,STW)
在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。
根區域掃描(root region scan)
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。
並發標記(Concurrent Marking)
G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷
最終標記(Remark,STW)
該階段是 STW 回收,幫助完成標記周期。G1 GC 清空 SATB 緩沖區,跟蹤未被訪問的存活對象,並執行引用處理。
清除垃圾(Cleanup,STW)
在這個最後階段,G1 GC 執行統計和 RSet 凈化的 STW 操作。在統計期間,G1 GC 會識別完全空閑的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閑列表時為部分並發。
提到並發標記,我們不得不了解並發標記的三色標記演算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。
根對象被置為黑色,子對象被置為灰色。
繼續由灰色遍歷,將已掃描了子對象的對象置為黑色。
遍歷了所有可達的對象後,所有可達的對象都變成了黑色。不可達的對象即為白色,需要被清理。
這看起來很美好,但是如果在標記過程中,應用程序也在運行,那麼對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題
我們看下面一種情況,當垃圾收集器掃描到下面情況時:
這時候應用程序執行了以下操作:
這樣,對象的狀態圖變成如下情形:
這時候垃圾收集器再標記掃描的時候就會下圖成這樣:
很顯然,此時C是白色,被認為是垃圾需要清理掉,顯然這是不合理的。那麼我們如何保證應用程序在運行的時候,GC標記的對象不丟失呢?有如下2中可行的方式:
在插入的時候記錄對象
在刪除的時候記錄對象
剛好這對應CMS和G1的2種不同實現方式:
在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)里發現要有一個白對象的引用被賦值到一個黑對象 的欄位里,那就把這個白對象變成灰色的。即插入的時候記錄下來。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:
這樣,G1到現在可以知道哪些老的分區可回收垃圾最多。 當全局並發標記完成後,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作「混合式」是因為他們不僅僅進行正常的新生代垃圾收集,同時也回收部分後台掃描線程標記的分區。混合式垃圾收集如下圖:
混合式GC也是採用的復制的清理策略,當GC完成後,會重新釋放空間。
至此,混合式GC告一段落了。下一小節我們講進入調優實踐。
MaxGCPauseMillis調優
前面介紹過使用GC的最基本的參數:
前面2個參數都好理解,後面這個MaxGCPauseMillis參數該怎麼配置呢?這個參數從字面的意思上看,就是允許的GC最大的暫停時間。G1盡量確保每次GC暫停的時間都在設置的MaxGCPauseMillis范圍內。 那G1是如何做到最大暫停時間的呢?這涉及到另一個概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區域集合。
Young GC:選定所有新生代里的region。通過控制新生代的region個數來控制young GC的開銷。
Mixed GC:選定所有新生代里的region,外加根據global concurrent marking統計得出收集收益高的若干老年代region。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代region。
在理解了這些後,我們再設置最大暫停時間就好辦了。 首先,我們能容忍的最大暫停時間是有一個限度的,我們需要在這個限度范圍內設置。但是應該設置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。如果MaxGCPauseMillis設置的過小,那麼GC就會頻繁,吞吐量就會下降。如果MaxGCPauseMillis設置的過大,應用程序暫停時間就會變長。G1的默認暫停時間是200毫秒,我們可以從這里入手,調整合適的時間。
其他調優參數
避免使用以下參數:
避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小。固定年輕代的大小會覆蓋暫停時間目標。
觸發Full GC
在某些情況下,G1觸發了Full GC,這時G1會退化使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時間將達到秒級別的。整個應用處於假死狀態,不能處理任何請求,我們的程序當然不希望看到這些。那麼發生Full GC的情況有哪些呢?
並發模式失敗
G1啟動標記周期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記周期。這種情形下,需要增加堆大小,或者調整周期(例如增加線程數-XX:ConcGCThreads等)。
晉升失敗或者疏散失敗
G1在進行GC的時候沒有足夠的內存供存活對象或晉升對象使用,由此觸發了Full GC。可以在日誌中看到(to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:
巨型對象分配失敗
當巨型對象找不到合適的空間進行分配時,就會啟動Full GC,來釋放空間。這種情況下,應該避免分配大量的巨型對象,增加內存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。
由於篇幅有限,G1還有很多調優實踐,在此就不一一列出了,大家在平常的實踐中可以慢慢探索。最後,期待java 9能正式發布,默認使用G1為垃圾收集器的java性能會不會又提高呢?
G1處理和傳統的垃圾收集策略是不同的,關鍵的因素是它將所有的內存進行了子區域的劃分。
總結
G1是一款非常優秀的垃圾收集器,不僅適合堆內存大的應用,同時也簡化了調優的工作。通過主要的參數初始和最大堆空間、以及最大容忍的GC暫停目標,就能得到不錯的性能;同時,我們也看到G1對內存空間的浪費較高,但通過**首先收集盡可能多的垃圾(Garbage First)的設計原則,可以及時發現過期對象,從而讓內存佔用處於合理的水平。
參考鏈接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796
⑷ java gc中為什麼復制演算法比標記整理演算法快
1、因為復制gc只需要把「活」的對象拷貝到survivor
2、復制演算法:兩個區域A和B,初始對象在A,繼續存活的對象被轉移到B。此為新生代最常用的演算法
標記清理:一塊區域,標記要回收的對象,然後回收,一定會出現碎片,那麼引出
標記-整理演算法:多了碎片整理,整理出更大的內存放更大的對象。
3、每次都是對其中的一塊進行內存回收,沒存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
⑸ JVM 技術詳解:常見的 GC 演算法(Parallel/CMS/G1)
學習了 GC 演算法的相關概念之後,我們將介紹在 JVM 中這些演算法的具體實現。首先要記住的是,大多數 JVM 都需要使用兩種不同的 GC 演算法——一種用來清理年輕代,另一種用來清理老年代。廳行
我們可以選擇 JVM 內置的各種演算法。如果不通過參數明確指定垃圾收集演算法,則會使用相應 JDK 版本的默認實現。本章會詳細介紹各種演算法的實現原理。
串列 GC 對年輕代使用 mark-(標記—復制)演算法,對老年代使用 mark-sweep-compact(標記—清除—整理)演算法。
兩者都是單線程的垃圾收集器,不能進行並行處理,所以都棗型會觸發全線暫停(STW),停止所有的應用線程。
因此這種 GC 演算法不能充分利用多核 CPU。不管有多少 CPU 內核,JVM 在垃圾收集時都只能使用單個核心。
要啟用此款收集器,只需要指定一個 JVM 啟動參數即可,同時對年輕代和老年代生效:
該選項只適合幾百 MB 堆內存的 JVM,而且是單核 CPU 時比較有用。
對於伺服器端來說,因為一般是多個 CPU 內核,並不推薦使用,除非確實需要限制 JVM 所使用的資源。大多數伺服器端應用部署在多核平台上,選擇 串列 GC 就意味著人為地限制了系統資源的使用,會導致資源閑置,多餘的 CPU 資源也不能用增加業務處理凳伏猜的吞吐量。
關於串列垃圾收集器的日誌內容,我們在後面的內容《GC 日誌解讀與分析》之中進行詳細的講解。
並行垃圾收集器這一類組合,在年輕代使用「標記—復制(mark-)演算法」,在老年代使用「標記—清除—整理(mark-sweep-compact)演算法」。年輕代和老年代的垃圾回收都會觸發 STW 事件,暫停所有的應用線程來執行垃圾收集。兩者在執行「標記和復制/整理」階段時都使用多個線程,因此得名「 Parallel 」。通過並行執行,使得 GC 時間大幅減少。
通過命令行參數 -XX:ParallelGCThreads=NNN 來指定 GC 線程數,其默認值為 CPU 核心數。可以通過下面的任意一組命令行參數來指定並行 GC:
並行垃圾收集器適用於多核伺服器,主要目標是增加吞吐量。因為對系統資源的有效使用,能達到更高的吞吐量:
⑹ java有哪些垃圾回收演算法
常用的垃圾回收演算法有:
(1).引用計數演算法:
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不再被使用的,垃圾收集器將回收該對象使用的內存。
引用計數演算法實現簡單,效率很高,微軟的COM技術、ActionScript、Python等都使用了引用計數演算法進行內存管理,但是引用計數演算法對於對象之間相互循環引用問題難以解決,因此java並沒有使用引用計數演算法。
(2).根搜索演算法:
通過一系列的名為「GC Root」的對象作為起點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Root沒有任何引用鏈相連時,則該對象不可達,該對象是不可使用的,垃圾收集器將回收其所佔的內存。
主流的商用程序語言C#、java和Lisp都使用根搜素演算法進行內存管理。
在java語言中,可作為GC Root的對象包括以下幾種對象:
a. java虛擬機棧(棧幀中的本地變數表)中的引用的對象。
b.方法區中的類靜態屬性引用的對象。
c.方法區中的常量引用的對象。
d.本地方法棧中JNI本地方法的引用對象。
java方法區在Sun HotSpot虛擬機中被稱為永久代,很多人認為該部分的內存是不用回收的,java虛擬機規范也沒有對該部分內存的垃圾收集做規定,但是方法區中的廢棄常量和無用的類還是需要回收以保證永久代不會發生內存溢出。
判斷廢棄常量的方法:如果常量池中的某個常量沒有被任何引用所引用,則該常量是廢棄常量。
判斷無用的類:
(1).該類的所有實例都已經被回收,即java堆中不存在該類的實例對象。
(2).載入該類的類載入器已經被回收。
(3).該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機制訪問該類的方法。
Java中常用的垃圾收集演算法:
(1).標記-清除演算法:
最基礎的垃圾收集演算法,演算法分為「標記」和「清除」兩個階段:首先標記出所有需要回收的對象,在標記完成之後統一回收掉所有被標記的對象。
標記-清除演算法的缺點有兩個:首先,效率問題,標記和清除效率都不高。其次,標記清除之後會產生大量的不連續的內存碎片,空間碎片太多會導致當程序需要為較大對象分配內存時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
(2).復制演算法:
將可用內存按容量分成大小相等的兩塊,每次只使用其中一塊,當這塊內存使用完了,就將還存活的對象復制到另一塊內存上去,然後把使用過的內存空間一次清理掉。這樣使得每次都是對其中一塊內存進行回收,內存分配時不用考慮內存碎片等復雜情況,只需要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
復制演算法的缺點顯而易見,可使用的內存降為原來一半。
(3).標記-整理演算法:
標記-整理演算法在標記-清除演算法基礎上做了改進,標記階段是相同的標記出所有需要回收的對象,在標記完成之後不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,在移動過程中清理掉可回收的對象,這個過程叫做整理。
標記-整理演算法相比標記-清除演算法的優點是內存被整理以後不會產生大量不連續內存碎片問題。
復制演算法在對象存活率高的情況下就要執行較多的復制操作,效率將會變低,而在對象存活率高的情況下使用標記-整理演算法效率會大大提高。
(4).分代收集演算法:
根據內存中對象的存活周期不同,將內存劃分為幾塊,java的虛擬機中一般把內存劃分為新生代和年老代,當新創建對象時一般在新生代中分配內存空間,當新生代垃圾收集器回收幾次之後仍然存活的對象會被移動到年老代內存中,當大對象在新生代中無法找到足夠的連續內存時也直接在年老代中創建。
⑺ JVM有哪些垃圾回收演算法
標記-清除,標記-復制,標記-整理
⑻ 你不得不知道的JVM 垃圾回收
一、四種引用方式
1.1 強引用
1.2 軟引用(SoftReference)
1.3 弱引用(WeakReference)
1.4 虛引用(PhantomReference)
二、如何判斷對象是垃圾
2.1 引用計數法
2.2 根可達性分析
三、垃圾回收演算法
3.1 標記-清除(mark-sweep)
3.2 標記-整理(mark-compact)
3.3 標記-復制(mark-)
四、垃圾收集器
4.1 分類及特點簡述
4.1.1 串列
4.1.2 吞吐量優先
4.1.3 響應時間優先
4.2 串列垃圾回收器詳述
4.2.1 Serial
4.2.2 Serial-Old
4.2.3 流程圖
4.3 吞吐量優先垃圾回收器詳述
4.3.1 JVM相關參數
4.3.2 流程圖
4.4、響應時間優先垃圾回收器詳述
4.4.1 JVM相關參數
4.4.2 流程圖
4.3.3 CMS的特點
五、G1垃圾回收器
5.1 相關JVM參數
5.2 特點
5.3 G1新生代垃圾回收
5.4 G1老年代垃圾回收
只有所有 GC Roots對象都不通過【強引用】引用該對象,該對象才可以被回收。
某個對象只要有一處引用關系,該對象的引用次數就加1,如果一個對象的引用次數為0,則說明該對象是垃圾。
優勢:實現簡單,效率較高
弊端:如果有一對對象之間形成了相互引用,但是這兩個對象都已經沒有被其它對象所引用了,正常情況下,這一對對象應該被作為垃圾回收掉,但是因為形成了相互引用導致無法被回收。
通過GC Root對象開始向下尋找,尋找不到的對象即說明沒有被引用,那麼這些沒有被引用的對象被認定為垃圾。
目前,如下對象可以作為GC Root對象:
很好理解,即在GC的放生時候,先對所有對象進行根可達性分析,藉此標記所有的垃圾對象;所有對象標記完畢之後會進行清理操作。
因此,總體來說,就是先標記再清除。
弊端;標記清除之後會產生大量不連續的內存碎片,碎片太多可能會導致程序運行過程中需要分配較大對象時,無法滿足分配要求導致GC操作。
該回收演算法操作過程基本等同於 標記-清除 演算法只不過,第二步有點區別,該種方式會在清除的過程中進行 整理 操作,這是最大的不同。
優勢:最終不會出現若干空間碎片而導致的空間浪費。
弊端:在整理過程中帶來的計算不可小覷。
該種方式與前兩種有較大的區別:
該種方式會將存儲區分成兩個部分,分別為From、To,其中From區域中可能存在著對象,而To區域始終為空,用做下一次接受數據做准備。
分別有兩個指針指向這兩個區域:From-Pointer、To-Pointer,
優點:這種演算法非常適合早生夕死的對象
缺點:始終有一塊內存區域是未使用的,造成空間的浪費。
特點:
特點:
特點:
JVM開關:-XX:+UseSerialGC = Serial + SerialOld
上圖是:CMS垃圾回收器在老年代GC的工作流程圖:
經過上面的文字分析,新生代的Region個數為所有Region個數的5%;這個數值其實是很小的,那麼當新生代Region不夠用的時候,JVM會劃分更多的Region個數給新生代;
當新生代的Region個數佔比所有Region個數超過 60% 時,就會進行一次新生代的垃圾回收。
新生代垃圾回收會造成STW。
具體的垃圾回收演算法同其它幾個新生代垃圾回收器一樣,新生代都使用復制演算法。
老年代垃圾回收觸發機制與參數-XX:InitaingHeapOccupancyPercent有關。
但是需要注意的是:這一次的老年代回收,其實是一次混合垃圾回收,會同時清理新生代、老年代、Humongous。
與新生代回收演算法一致,依然使用復制演算法,但是垃圾回收的過程等同於老年代響應時間優先的CMS方式
流程分為:
⑼ java常見gc演算法有哪些
1:標記—清除 Mark-Sweep
過程:標記可回收對象,進行清除
缺點:標記和清除效率低,清除後會產生內存碎片
2:復制演算法
過程:將內存劃分為相等的兩塊,將存活的對象復制到另一塊內存,把已經使用的內存清理掉
缺點:使用的內存變為了原來的一半
進化:將一塊內存按8:1的比例分為一塊Eden區(80%)和兩塊Survivor區(10%)
每次使用Eden和一塊Survivor,回收時,將存活的對象一次性復制到另一塊Survivor上,如果另一塊Survivor空間不足,則使用分配擔保機制存入老年代
3:標記—整理 Mark—Compact
過程:所有存活的對象向一端移動,然後清除掉邊界以外的內存
4:分代收集演算法
過程:將堆分為新生代和老年代,根據區域特點選用不同的收集演算法,如果新生代朝生夕死,則採用復制演算法,老年代採用標記清除,或標記整理
面試的話說出來這四種足夠了