jvm垃圾收集器按不同的角度似乎有幾種分法。例如,按收集的區(qū)域有收集新生代和老生代的分別,按收集時是否多線程有串行和并行的分別,按是否會停止jvm中用戶線程的運行有并發(fā)與非并發(fā)的分別??梢娨粋€垃圾收集器具有多種屬性。
從官方的一些文檔看,一般將Hotspot JVM的垃圾收集器分為三類
- Serial GC 串行收集器
- Parallel GC 并行收集器
- the mostly Concurrent GC 基本并發(fā)收集器
對于上面提到的串行與并行,具體在gc的領(lǐng)域里,其含義如下:
串行 Serial vs. 并行 Parallel
- 串行:只使用一個線程執(zhí)行垃圾收集
- 并行:使用多個線程執(zhí)行垃圾收集
并發(fā)與非并發(fā)則具體是:
Stop the World vs. 并發(fā) Concurrent
- Stop the World 指在執(zhí)行垃圾收集時,jvm會停止應(yīng)用的用戶線程,導(dǎo)致應(yīng)用出現(xiàn)停頓
- 并發(fā)Concurrent則與STW相反,垃圾收集的過程可以和用戶線程同時執(zhí)行。
除此之外,不同的gc實現(xiàn)還有incremental和monolithic的區(qū)別,具體來說就是
Incremental vs. Monolithic
- Incremental 指gc以一系列的分段的步驟執(zhí)行垃圾回收過程,使得用戶線程可以在各步驟之間運行。通常也和STW聯(lián)合使用。
- Monolithic 則和Incremental相反,gc的過程是整體執(zhí)行的,它總是引起STW、停止用戶線程的執(zhí)行,直到垃圾收集過程結(jié)束
最后,gc中還有Precise和Conservative的區(qū)分
Precise vs. Conservative
- Precise 指gc在收集的過程中,可以完全識別、處理所有對象的引用reference。一個垃圾收集器如果會移動對象在內(nèi)存中的位置,那么它必須是Precise的,以免遺漏處理對象導(dǎo)致內(nèi)存被破壞??梢哉f,所有商用的服務(wù)端JVM都使用Precise的收集器,并在它們的某些過程中會移動對象。
- Conservative 相對的則對于某些對象引用是不知曉的。某些語言和系統(tǒng)使用了這種機制,比如C++
分代收集
JVM垃圾收集器的一大特點就是使用分代收集。在運行期,java對象處于JVM堆內(nèi)存中,而JVM將這部分內(nèi)存分成了不同的區(qū)域,即常說得新生代和老生代,而對于像G1這樣的新一代收集器,它進一步將其所收集的內(nèi)存區(qū)域分成更小的Region,這一點后面再說。
無論是分成新生代老生代,還是更小的Region,之所以使用分代的方式進行垃圾收集,其原因是基于一個實踐中觀察到的結(jié)果,即對于大多數(shù)應(yīng)用程序,運行期創(chuàng)建的對象大多只存活很短的一段時間,相應(yīng)的,只有少量的對象會在內(nèi)存中持續(xù)很長時間,這個結(jié)果被稱作“the weak generational hypotheis”。
大致上,分代收集會按下面的算法進行對象在內(nèi)存中的處理。新創(chuàng)建的對象都分配在新生代上,這些對象每熬過一次gc,其“年齡”就會增加,當對象的年齡增加到一定程度后,虛擬機將把這些對象轉(zhuǎn)移到老生代中。
正是由于對象存活時間存在不同,采用分代管理后,JVM可以針對不同區(qū)域里對象的特點采用不同的算法進行有效的收集。例如,對于新生代,其中大量的對象在收集時都會死去,就適合用后面提到的“復(fù)制算法”,而老生代中的對象存活時間長,就適合后面提到的“標記 - 清除”、“標記 - 整理”算法。
采用分代收集后,對于新生代收集的停頓時間會大大短于老生代收集的停頓時間,這使得可以降低老生代的停頓頻率(但停頓的持續(xù)時間不一定會減少)
永久代
在Java 8之前,除了新生代、老生代之外,java管理的堆中還有一部分叫“永生代”的區(qū)域。實際上,永生代的說法是針對HotSpot虛擬機而言的。這部分區(qū)域,用于存放虛擬機加載的類信息、常量、靜態(tài)變量、運行時常量池等。嚴格的講,從JVM規(guī)范的角度看,這個區(qū)域應(yīng)該叫做“方法區(qū)”,而HotSpot虛擬機只是剛好把這個方法區(qū)實現(xiàn)為gc分代中的一種而已。
對于HotSpot虛擬機而言,永久代是一個帶來很多麻煩的地方。其當初設(shè)計時的一些假設(shè)在實際證明是不正確的,尤其是在目前有許多應(yīng)用使用自定義的ClassLoader的情況下。
因此,HotSpot虛擬機逐步的對永生代進行改進。在Java 7中,將字符串String從永久代中的存放改為了存放在老生代中。進一步的,到了Java 8,永生代直接被廢除。這里的廢除并不是說不需要存儲這部分數(shù)據(jù)了,而是將其實現(xiàn)脫離分代管理的機制,轉(zhuǎn)為用本地內(nèi)存native memory來存儲,并且改名為元空間Metaspace。由于永生代的大小需要在jvm啟動前就通過參數(shù)顯式指定其大小(默認的是64MB,對于64 bit scaled pointer則是85MB),并且在運行中不能改變其大小,同時由于很難準確估計永生代到底需要多少內(nèi)存空間,因此常常出現(xiàn)OOM內(nèi)存溢出的異常。而元空間由于脫離了堆實現(xiàn)的限制后,使用native memory,因此其實際內(nèi)存大小就是整個內(nèi)存空間的可用大小,因而它不會再像永生代那樣容易發(fā)生OOM了。
同時,由于永生代是和老生代有捆綁關(guān)系的,即當老生代滿了的時候,進行full gc,永生代也會一并觸發(fā)回收。改用元空間后,解綁了這層關(guān)系,使得full gc的處理可以進行簡化。
Remembered Set
分代的目的是對不同存活時間的對象分開處理,以提高垃圾收集的效率,減少停頓時間。但這樣實現(xiàn)后,會帶來一些額外的問題。例如,收集新生代中的對象時,JVM需要判斷該對象是不是被老生代中的對象引用了,這可能導(dǎo)致收集新生代需要掃描老生代。
為了減少上面對老生代的掃描時間,JVM引入了一個叫Remembered Set的集合,用于記錄所有從老生代引用新生代對象的信息。這樣,收集新生代時,只需要檢查RS,而不用掃描整個老生代了。這時,RS也可以被看作是新生代gc的roots之一。
RS在大部分收集器中,使用一個叫CardTable的表進行跟蹤。CardTable可能會使用byte或bit來表示某些老生代是否有新生代的引用,這種實現(xiàn)使得檢查起來很快。但是需要注意的是,即使CardTable內(nèi)是松散填充的(意味著有較少的老生代引用了新生代),在檢查時,仍然需要檢查整個CardTable。這也意味著老生代的堆大小增長,會影響CardTable的檢查時間,進而影響新生代收集的時間。
收集機制
Precise的垃圾收集器,使用跟蹤算法進行收集,跟蹤算法相比引用計數(shù)算法,可以更有效回收對象,避免遺漏,尤其是對具有循環(huán)引用的對象組,可做到安全、精準的收集。
跟蹤算法的收集器可能使用三種技術(shù)
mark / sweep / compact
即常說的 “標記 - 清除”算法copying
即常說的“復(fù)制”算法mark / compact
即常說的“標記 - 整理”算法
從上面可以看出,這三種技術(shù)其實是一些更具體的手段的組合。下面分別說一下這些具體的方法
1. Mark
Mark也叫Trace,即所說的跟蹤,它是一種可以找到堆中所有存活對象的方法。當進行垃圾收集時,收集器從gc的roots開始查找所有活著(可達)的對象,對其進行標記,以備后續(xù)階段的進一步操作。
這里的gc roots,通常包括:
- static variables
- registers
- thread stacks上的內(nèi)容
- remembered set
標記階段的耗時隨著存活對象集的大小線性增長,也就是說與堆本身的大小沒有關(guān)系。
2. Sweep
Sweep是對象的清除階段,它不一定是真正的將對象抹去,有可能是將被回收對象的內(nèi)存記在空閑列表中表示這塊區(qū)域可以被重新使用,也可能是對其做某種處理以便被后面的compact階段使用。
其復(fù)雜度隨堆的大小影響,因為Sweep過程需要覆蓋整個堆。
3. Compact / Relocate
壓縮是為了避免內(nèi)存出現(xiàn)碎片,因此JVM總會使用壓縮。壓縮可分為兩種形式
- in-place
在壓縮的內(nèi)存區(qū)域內(nèi)進行對象的移動,將所有對象移動到堆的一側(cè),以便清理出連續(xù)的空閑空間。 - evacuating
把一個區(qū)域Region的對象移動到另一個外部空區(qū)域中,然后清空原區(qū)域。實際上,這個和后面說到的copy很像,目前資料看來,evacuating是專門針對G1收集器而言的。
4. Copy
Copy復(fù)制算法一般針對新生代的垃圾收集使用,它將新生代的堆分成 from 和 to 兩個區(qū)域。每次垃圾收集時,to區(qū)域總是空的,gc對 from 區(qū)域進行對象的trace,確定存活的對象,然后將存活的對象copy到 to 區(qū)域,并反轉(zhuǎn)當前 to 和 from 的角色,使得原來的 from 區(qū)域變成 to,即為新的空區(qū)域。
復(fù)制算法通常是Monolithic的,因為它要保證整個過程的一致、完整性,不允許存在中間狀態(tài)。這樣,通常要求from 和 to兩個區(qū)域的大小是一致的,否則可能出現(xiàn)to區(qū)域容納不下from區(qū)域內(nèi)存活對象的情況。
但是,這樣又導(dǎo)致總有一半的新生代內(nèi)存區(qū)域是空閑的,使得內(nèi)存使用率不高。
針對這種情況,復(fù)制算法的垃圾收集器可以有優(yōu)化的地方。而優(yōu)化的基礎(chǔ),就是分代收集。據(jù)IBM的研究,98%的對象都是朝生夕死的,因此并不需要1:1的比例分成from和to區(qū)域。對于HotSpot虛擬機,從一開始就使用了優(yōu)化版的復(fù)制算法,它將內(nèi)存區(qū)域分成3部分,一個較大的Eden區(qū),兩個較小的Survivor區(qū)域。每個新創(chuàng)建的對象都分配到Eden區(qū),當垃圾回收時,總是回收Eden區(qū)和一個Survivor區(qū),并將這兩個區(qū)域中存活的對象都復(fù)制到剩下的那個Survivor區(qū)域里。收集完成后,之前的Survivor區(qū)和Eden區(qū)被清空,之前的Survivor區(qū)就變?yōu)橄麓蝕c時對象要拷貝到的區(qū)域了。
Eden和2個Survivor的比例在HotSpot中默認是8:1:1。因此只有10%的內(nèi)存會被空閑出來。另外前面說到的98%的對象可以被回收并不是絕對的,因此可能出現(xiàn)回收后Survivor區(qū)容納不下存活對象的情況,這時分代收集的好處就體現(xiàn)出來了。由于有老生代的存在,多出來的存活對象可以被提前提升到老生代中,這個機制被稱作內(nèi)存擔保。