一、常用垃圾回收機制
1. 標記-清除算法(mark-sweep)
顧名思義,標記-清除算法分為兩個階段,標記(mark)和清除(sweep).
在標記階段,collector從mutator根對象開始進行遍歷,對從mutator根對象可以訪問到的對象都打上一個標識,一般是在對象的header中,將其記錄為可達對象。
而在清除階段,collector對堆內存(heap memory)從頭到尾進行線性的遍歷,如果發現某個對象沒有標記為可達對象-通過讀取對象的header信息,則就將其回收。
[圖片上傳失敗...(image-8c300b-1510574775013)]
從上圖我們可以看到,在Mark階段,從根對象1可以訪問到B對象,從B對象又可以訪問到E對象,所以B,E對象都是可達的。同理,F,G,J,K也都是可達對象。到了Sweep階段,所有非可達對象都會被collector回收。同時,Collector在進行標記和清除階段時會將整個應用程序暫停(mutator),等待標記清除結束后才會恢復應用程序的運行。
缺點:
? 標記-清除算法的比較大的缺點就是垃圾收集后有可能會造成大量的內存碎片,像上面的圖片所示,垃圾收集后內存中存在三個內存碎片,假設一個方格代表1個單位的內存,如果有一個對象需要占用3個內存單位的話,那么就會導致Mutator一直處于暫停狀態,而Collector一直在嘗試進行垃圾收集,直到Out of Memory。
2. 標記-壓縮算法(mark-compact)
? 顧名思義,標記-壓縮算法分為兩個階段,標記(mark)和壓縮(compact).
? 其中標記階段跟標記-清除算法中的標記階段是一樣的,而對于壓縮階段,它的工作就是移動所有的可達對象到堆內存的同一個區域中,使他們緊湊的排列在一起,從而將所有非可達對象釋放出來的空閑內存都集中在一起,通過這樣的方式來達到減少內存碎片的目的。
3. 復制算法(copying)
堆內存對半分為兩個半區,只用其中一個半區來進行對象內存的分配,如果在這個半區內存不夠給新的對象分配了,那么就開始進行垃圾收集,將這個半區中的所有可達對象都拷貝到另外一個半區中去,然后繼續在另外那個半區進行新對象的內存分配。
缺點:
? 存壓縮為原來的一半,利用率比較低,典型的空間換時間
4. 引用計數算法(reference counting)
? 通過在對象頭中分配一個空間來保存該對象被引用的次數。如果該對象被其它對象引用,則它的引用計數加一,如果刪除對該對象的引用,那么它的引用計數就減一,當該對象的引用計數為0時,那么該對象就會被回收。
? 采用引用計數的垃圾收集機制跟前面三種垃圾收集機制最大的不同在于,垃圾收集的開銷被分攤到整個應用程序的運行當中了,而不是在進行垃圾收集時,要掛起整個應用的運行,直到對堆中所有對象的處理都結束。因此,采用引用計數的垃圾收集不屬于嚴格意義上的"Stop-The-World"的垃圾收集機制。
注意:
當某個對象的引用計數減為0時,collector需要遞歸遍歷它所指向的所有域,將它所有域所指向的對象的引用計數都減一,然后才能回收當前對象。
-
但是這種引用計數算法有一個比較大的問題,那就是它不能處理環形數據 - 即如果有兩個對象相互引用,那么這兩個對象就不能被回收,因為它們的引用計數始終為1。這也就是我們常說的“內存泄漏”問題。如下圖:
[圖片上傳失敗...(image-56fb94-1510574775013)]
5. 分代收集算法
? 當前的商業虛擬機都采用的是”分代收集“算法,一般是把java堆分成新生代和老生代,這樣就可以根據各個年代的特點采用最適當的垃圾收集算法,新生代中,對象大多是”朝生夕死“可以采用復制算法,而老年代的對象存活率比較高,而且沒有擔保空間進行內存分配,就要采用”標記-清除算法“或者”標記-整理“算法。
二、Java垃圾回收
1. Java的內存分布
[圖片上傳失敗...(image-5469f-1510574775013)]
其中,堆內存分為年輕代和年老代,非堆內存主要是Permanent區域,主要用于存儲一些類的元數據,常量池等信息。而年輕代又分為兩種,一種是Eden區域,另外一種是兩個大小對等的Survivor區域。
2. Java年輕代垃圾回收機制
[圖片上傳失敗...(image-3bdb3-1510574775013)]
? 部分的新創建對象分配在新生代。因為大部分對象很快就會變得不可達,所以它們被分配在新生代,然后消失不再。當對象從新生代移除時,我們稱之為"Minor GC"。新生代使用的是復制收集算法。
? 新生代劃分為三個部分:分別為Eden、Survivor from、Survivor to,大小比例為8:1:1(為了防止復制收集算法的浪費內存過大)。每次只使用Eden和其中的一塊Survivor,回收時將存活的對象復制到另一塊Survivor中,這樣就只有10%的內存被浪費,但是如果存活的對象總大小超過了Survivor的大小,那么就把多出的對象放入老年代中。
在三個區域中有兩個是Survivor區。對象在三個區域中的存活過程如下:
- 大多數新生對象都被分配在Eden區。
- 第一次GC過后Eden中還存活的對象被移到其中一個Survivor區。
- 再次GC過程中,Eden中還存活的對象會被移到之前已移入對象的Survivor區。
- 一旦該Survivor區域無空間可用時,還存活的對象會從當前Survivor區移到另一個空的Survivor區。而當前Survivor區就會再次置為空狀態。
- 經過數次(默認是15次)在兩個Survivor區域移動后還存活的對象最后會被移動到老年代。
如上所述,兩個Survivor區域在任何時候必定有一個保持空白。如果同時有數據存在于兩個Survivor區或者兩個區域的的使用量都是0,則意味著你的系統可能出現了運行錯誤。
3. Java老年代垃圾回收機制
? 存活在新生代中但未變為不可達的對象會被復制到老年代。一般來說老年代的內存空間比新生代大,所以在老年代GC發生的頻率較新生代低一些。當對象從老年代被移除時,我們稱之為 "Major GC"(或者Full GC)。 老年代使用標記-清理或標記-整理算法
空間分配擔保
在發生Minor GC前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間。
如果大于,那么Minor GC可以確保是安全的。
-
如果小于,虛擬機會查看HandlePromotionFailure設置值是否允許擔任失敗。
- 如果允許,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升老年代對象的平均大小
- 如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的
- 如果小于,進行一次Full GC
- 如果不允許,也要改為進行一次Full GC
? 前面提到過,新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC后仍然存活的情況時(最極端就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,讓Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來,在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
? 取平均值進行比較其實仍然是一種動態概率的手段,也就是說如果某次Minor GC存活后的對象突增,遠遠高于平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁。
- 如果允許,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升老年代對象的平均大小
2.Java垃圾收集器
-
Serial收集器(Serial/Serial Old)
Serial是一個單線程的收集器,但它的“單線程”意義并不僅僅說明它只會使用一個CPU或一條手機此案成去完成垃圾和收集工作,更重要的是它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
img -
ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本。
它是運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合工作。
img -
Parallel Scavenge收集器
? 該收集器也是一個新生代的垃圾收集器,他也是使用復制算法的收集器,又是一個并行的垃圾收集器。該收集器的特點是他的關注點與其他的收集器不同,CMS等收集器的關注點是盡可能縮短垃圾回收時用戶線程的停頓時間,而parallel Scavenge收集器的目標是達到一個可控制的吞吐量。所謂吞吐量就是CPU用于運行代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間),比如虛擬機總共運行100分鐘,垃圾回收占用了1分鐘,那么吞吐量就是99%。
-
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。
img -
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS是基于“標記-清除”算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:
- 初始標記(CMS initial mark)
- 并發標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發清除(CMS concurrent sweep)
其中,初始標記、重新標記這兩個步驟仍然需要”Stop The world”。初始標記僅僅只是標記一下GC Roots Tracing的過程,而重新標記階段則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
由于整個過程中耗時最長的并發標記和并發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。
imgCMS的優勢:并發收集、低停頓。
CMS的缺點:
- 對CPU資源非常敏感。CMS默認啟動的回收線程數是(CPU數量 + 3)/4,并發回收時垃圾收集線程所占CPU資源隨著CPU數量的增加而下降,而且在CPU不足4個時,CMS對用戶程序的影響就可能變得很大,導致執行速度降低。
- CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
- CMS是一款基于“標記-清除”算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。空間碎片太多的時候,將會給大對象分配帶來很大麻煩。
-
G1收集器
G1是一款面向服務端應用的垃圾收集器。HOtSpot開發團隊賦予它的使命是未來可以替換掉CMS收集器。
G1具備如下特點:
- 并行與并發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續執行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的就對象以獲取更好的收集效果。
- 空間整合:G1從整體上來看是基于“標記-整理”算法實現的收集器,從局部(兩個Region之間)上來看是基于“復制”算法實現的,這意味著G1運作期間不會產生內存空間碎片,收集后能提供規整的可用內存。
- 可預測的停頓:這是G1相對于CMS的另一大優勢。
?
G1垃圾收集器和CMS垃圾收集器有幾點不同。首先,最大的不同是內存的組織方式變了。Eden,Survivor和Tenured等內存區域不再是連續的了,而是變成了一個個大小一樣的region - 每個region從1M到32M不等。
?
[圖片上傳失敗...(image-f2affc-1510574775013)]
?
一個region有可能屬于Eden,Survivor或者Tenured內存區域。圖中的E表示該region屬于Eden內存區域,S表示屬于Survivor內存區域,T表示屬于Tenured內存區域。圖中空白的表示未使用的內存空間。G1垃圾收集器還增加了一種新的內存區域,叫做Humongous內存區域,如圖中的H塊。這種內存區域主要用于存儲大對象-即大小超過一個region大小的50%的對象。
?
在G1垃圾收集器中,年輕代的垃圾回收過程跟PS垃圾收集器和CMS垃圾收集器差不多。
[圖片上傳失敗...(image-5a0154-1510574775013)]
?
對于年老代上的垃圾收集,G1垃圾收集器也分為4個階段,基本跟CMS垃圾收集器一樣,但略有不同:
Initial Mark階段 - 同CMS垃圾收集器的Initial Mark階段一樣,G1也需要暫停應用程序的執行,它會標記從根對象出發,在根對象的第一層孩子節點中標記所有可達的對象。但是G1的垃圾收集器的Initial Mark階段是跟minor gc一同發生的。也就是說,在G1中,你不用像在CMS那樣,單獨暫停應用程序的執行來運行Initial Mark階段,而是在G1觸發minor gc的時候一并將年老代上的Initial Mark給做了。
Concurrent Mark階段 - 在這個階段G1做的事情跟CMS一樣。但G1同時還多做了一件事情,那就是,如果在Concurrent Mark階段中,發現哪些Tenured region中對象的存活率很小或者基本沒有對象存活,那么G1就會在這個階段將其回收掉,而不用等到后面的clean up階段。這也是Garbage First名字的由來。同時,在該階段,G1會計算每個 region的對象存活率,方便后面的clean up階段使用 。
Remark階段 - 在這個階段G1做的事情跟CMS一樣, 但是采用的算法不同,能夠在Remark階段更快的標記可達對象。
-
Clean up/Copy階段 - 在G1中,沒有CMS中對應的Sweep階段。相反 它有一個Clean up/Copy階段,在這個階段中,G1會挑選出那些對象存活率低的region進行回收,這個階段也是和minor gc一同發生的,如下圖所示:
[圖片上傳失敗...(image-ed332d-1510574775013)]
從上可以看到,由于Initial Mark階段和Clean up/Copy階段都是跟minor gc同時發生的,相比于CMS,G1暫停應用程序的時間更少,從而提高了垃圾回收的效率。
?