引言:
前面的文章提到,在8版本以后,Java內存區域:Heap包括了PSYoungGen、ParOldGen,以及堆外內存MetaSpace。JVM 在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是新生代。由於新生代和老年代的內存空間大小不同以及對象存活率不同,所以針對不同區域JVM采用了不同的GC,不同的GC是通過不同的算法實現的。在Jdk8中,按照回收區域的不同,把GC分為工作在新生代的普通GC(minor GC)和工作在堆全局空間的全局GC(Full GC)。
由於新生代和老年代占比空間為1:2,且采用了不同的算法,所以minor GC 的速度要比Full GC快很多。
一、復制算法
HotSpot 把新生代分為三個部分:Eden區和兩個Survivor區(From區和To區),默認比例8:1:1。對象創建時會被放在Eden區,當Eden區觸發GC(minor GC),GC會對Eden和Survivor區進行垃圾回收,幸存下來的獨享會被 “復制” 到Survivor1區(To區),然后清空Eden和From區,最后將To和From交換,讓剛才被清空的From作新的To區,讓剛才保存對象的To區作新的From區,以保證下一次GC可以掃描到這些對象。這個過程中涉及到了一個 “復制” 的操作,就是 “復制算法” 的實現。順帶一提:當一個對象在多次GC后依然無法被回收,在From區和To區來回復制,每復制一次“年齡”加1,一旦“年齡”達到MaxTenuringThreshold的值(默認為15)就會被移動到老年代。
為了方便描述,這里將minor GC的掃描區域(Eden、From)簡稱為From區,因為這兩塊區域的共同特點就是在復制幸存對象到To區后會被清空,唯一的區別就是Eden用來保存第一次new出來的對象,而From區保存的則是經過若干次GC后任然幸存的對象。
整個流程如下圖所示:

紅色為幸存對象,黃色為被GC回收的對象,綠色表示閑置空間。當觸發GC后,Eden區和From區的幸存對象會被復制到To區,然后清空Eden區和From區,最后將From區和To區對調,以保證下一次GC的正常工作流程。這些內容在前面介紹堆參數時也有提及,這里不再贅述。需要補充的是 “復制算法” 的優缺點:
優點:1、由於“復制算法”采用了復制—清空的方法,所以不會導致內存空間的碎片化。
缺點:1、由於復制算法需要另外的空間來 “周轉” 這些幸存的對象,所以內存消耗比較大。
2、如果存在“極端情況”,比如大量的對象循環引用而導致無法回收的幸存對象占比很大,假設為80%,那么就需要將這些數量龐大的對象都復制一遍,並將所有的引用地址重置一遍,這回耗費比較多的時間。所以復制算法的最佳工作環境就是這一塊的對象存活率比較低,所以在新生代中采用了這一算法。
二、標記清除
這是GC在老年代中的工作方式,標記清除算法分為兩個階段:標記階段和清除階段。

這種算法雖然不需要多余的內存空間 “周轉” 對象,但是會導致內存碎片化。於是便引出了標記壓縮算法
三、標記壓縮
標記壓縮其實就是在標記清除后加了一個 “壓縮” 操作,將分散的數據壓縮到一塊連續的內存空間。就是慢,但慢工出細活。

四、更加優秀的選擇
針對老年的GC,標記清除和標記壓縮都不完美,最好的方式是組合使用,在多次使用標記清除后進行一次壓縮。總的來說四種方式沒有孰優孰劣,只有誰更合適。總結一下就是:
執行效率(算法的時間復雜度):復制算法>標記清除>標記壓縮
內存整齊度:復制算法=標記壓縮>標記清除
內存利用率:標記清除=標記壓縮>復制算法
在Java9默認采用了G1垃圾回收器,采用了時間復雜度和空間利用率都非常出色的算法。
