垃圾收集器(一)

jvm垃圾收集器按不同的角度似乎有幾種分法。例如,按收集的區域有收集新生代和老生代的分別,按收集時是否多線程有串行和并行的分別,按是否會停止jvm中用戶線程的運行有并發與非并發的分別。可見一個垃圾收集器具有多種屬性。

從官方的一些文檔看,一般將Hotspot JVM的垃圾收集器分為三類

  • Serial GC 串行收集器
  • Parallel GC 并行收集器
  • the mostly Concurrent GC 基本并發收集器

對于上面提到的串行與并行,具體在gc的領域里,其含義如下:

串行 Serial vs. 并行 Parallel

  • 串行:只使用一個線程執行垃圾收集
  • 并行:使用多個線程執行垃圾收集

并發與非并發則具體是:

Stop the World vs. 并發 Concurrent

  • Stop the World 指在執行垃圾收集時,jvm會停止應用的用戶線程,導致應用出現停頓
  • 并發Concurrent則與STW相反,垃圾收集的過程可以和用戶線程同時執行。

除此之外,不同的gc實現還有incremental和monolithic的區別,具體來說就是

Incremental vs. Monolithic

  • Incremental 指gc以一系列的分段的步驟執行垃圾回收過程,使得用戶線程可以在各步驟之間運行。通常也和STW聯合使用。
  • Monolithic 則和Incremental相反,gc的過程是整體執行的,它總是引起STW、停止用戶線程的執行,直到垃圾收集過程結束

最后,gc中還有Precise和Conservative的區分

Precise vs. Conservative

  • Precise 指gc在收集的過程中,可以完全識別、處理所有對象的引用reference。一個垃圾收集器如果會移動對象在內存中的位置,那么它必須是Precise的,以免遺漏處理對象導致內存被破壞。可以說,所有商用的服務端JVM都使用Precise的收集器,并在它們的某些過程中會移動對象。
  • Conservative 相對的則對于某些對象引用是不知曉的。某些語言和系統使用了這種機制,比如C++

分代收集

JVM垃圾收集器的一大特點就是使用分代收集。在運行期,java對象處于JVM堆內存中,而JVM將這部分內存分成了不同的區域,即常說得新生代和老生代,而對于像G1這樣的新一代收集器,它進一步將其所收集的內存區域分成更小的Region,這一點后面再說。

無論是分成新生代老生代,還是更小的Region,之所以使用分代的方式進行垃圾收集,其原因是基于一個實踐中觀察到的結果,即對于大多數應用程序,運行期創建的對象大多只存活很短的一段時間,相應的,只有少量的對象會在內存中持續很長時間,這個結果被稱作“the weak generational hypotheis”。

大致上,分代收集會按下面的算法進行對象在內存中的處理。新創建的對象都分配在新生代上,這些對象每熬過一次gc,其“年齡”就會增加,當對象的年齡增加到一定程度后,虛擬機將把這些對象轉移到老生代中。

正是由于對象存活時間存在不同,采用分代管理后,JVM可以針對不同區域里對象的特點采用不同的算法進行有效的收集。例如,對于新生代,其中大量的對象在收集時都會死去,就適合用后面提到的“復制算法”,而老生代中的對象存活時間長,就適合后面提到的“標記 - 清除”、“標記 - 整理”算法。

采用分代收集后,對于新生代收集的停頓時間會大大短于老生代收集的停頓時間,這使得可以降低老生代的停頓頻率(但停頓的持續時間不一定會減少)

永久代

在Java 8之前,除了新生代、老生代之外,java管理的堆中還有一部分叫“永生代”的區域。實際上,永生代的說法是針對HotSpot虛擬機而言的。這部分區域,用于存放虛擬機加載的類信息、常量、靜態變量、運行時常量池等。嚴格的講,從JVM規范的角度看,這個區域應該叫做“方法區”,而HotSpot虛擬機只是剛好把這個方法區實現為gc分代中的一種而已。

對于HotSpot虛擬機而言,永久代是一個帶來很多麻煩的地方。其當初設計時的一些假設在實際證明是不正確的,尤其是在目前有許多應用使用自定義的ClassLoader的情況下。

因此,HotSpot虛擬機逐步的對永生代進行改進。在Java 7中,將字符串String從永久代中的存放改為了存放在老生代中。進一步的,到了Java 8,永生代直接被廢除。這里的廢除并不是說不需要存儲這部分數據了,而是將其實現脫離分代管理的機制,轉為用本地內存native memory來存儲,并且改名為元空間Metaspace。由于永生代的大小需要在jvm啟動前就通過參數顯式指定其大小(默認的是64MB,對于64 bit scaled pointer則是85MB),并且在運行中不能改變其大小,同時由于很難準確估計永生代到底需要多少內存空間,因此常常出現OOM內存溢出的異常。而元空間由于脫離了堆實現的限制后,使用native memory,因此其實際內存大小就是整個內存空間的可用大小,因而它不會再像永生代那樣容易發生OOM了。

同時,由于永生代是和老生代有捆綁關系的,即當老生代滿了的時候,進行full gc,永生代也會一并觸發回收。改用元空間后,解綁了這層關系,使得full gc的處理可以進行簡化。

Remembered Set

分代的目的是對不同存活時間的對象分開處理,以提高垃圾收集的效率,減少停頓時間。但這樣實現后,會帶來一些額外的問題。例如,收集新生代中的對象時,JVM需要判斷該對象是不是被老生代中的對象引用了,這可能導致收集新生代需要掃描老生代。

為了減少上面對老生代的掃描時間,JVM引入了一個叫Remembered Set的集合,用于記錄所有從老生代引用新生代對象的信息。這樣,收集新生代時,只需要檢查RS,而不用掃描整個老生代了。這時,RS也可以被看作是新生代gc的roots之一。

RS在大部分收集器中,使用一個叫CardTable的表進行跟蹤。CardTable可能會使用byte或bit來表示某些老生代是否有新生代的引用,這種實現使得檢查起來很快。但是需要注意的是,即使CardTable內是松散填充的(意味著有較少的老生代引用了新生代),在檢查時,仍然需要檢查整個CardTable。這也意味著老生代的堆大小增長,會影響CardTable的檢查時間,進而影響新生代收集的時間。

收集機制

Precise的垃圾收集器,使用跟蹤算法進行收集,跟蹤算法相比引用計數算法,可以更有效回收對象,避免遺漏,尤其是對具有循環引用的對象組,可做到安全、精準的收集。

跟蹤算法的收集器可能使用三種技術

  • mark / sweep / compact
    即常說的 “標記 - 清除”算法

  • copying
    即常說的“復制”算法

  • mark / compact
    即常說的“標記 - 整理”算法

從上面可以看出,這三種技術其實是一些更具體的手段的組合。下面分別說一下這些具體的方法

1. Mark

Mark也叫Trace,即所說的跟蹤,它是一種可以找到堆中所有存活對象的方法。當進行垃圾收集時,收集器從gc的roots開始查找所有活著(可達)的對象,對其進行標記,以備后續階段的進一步操作。

這里的gc roots,通常包括:

  • static variables
  • registers
  • thread stacks上的內容
  • remembered set

標記階段的耗時隨著存活對象集的大小線性增長,也就是說與堆本身的大小沒有關系。

2. Sweep

Sweep是對象的清除階段,它不一定是真正的將對象抹去,有可能是將被回收對象的內存記在空閑列表中表示這塊區域可以被重新使用,也可能是對其做某種處理以便被后面的compact階段使用。

其復雜度隨堆的大小影響,因為Sweep過程需要覆蓋整個堆。

3. Compact / Relocate

壓縮是為了避免內存出現碎片,因此JVM總會使用壓縮。壓縮可分為兩種形式

  • in-place
    在壓縮的內存區域內進行對象的移動,將所有對象移動到堆的一側,以便清理出連續的空閑空間。
  • evacuating
    把一個區域Region的對象移動到另一個外部空區域中,然后清空原區域。實際上,這個和后面說到的copy很像,目前資料看來,evacuating是專門針對G1收集器而言的。

4. Copy

Copy復制算法一般針對新生代的垃圾收集使用,它將新生代的堆分成 from 和 to 兩個區域。每次垃圾收集時,to區域總是空的,gc對 from 區域進行對象的trace,確定存活的對象,然后將存活的對象copy到 to 區域,并反轉當前 to 和 from 的角色,使得原來的 from 區域變成 to,即為新的空區域。

復制算法通常是Monolithic的,因為它要保證整個過程的一致、完整性,不允許存在中間狀態。這樣,通常要求from 和 to兩個區域的大小是一致的,否則可能出現to區域容納不下from區域內存活對象的情況。

但是,這樣又導致總有一半的新生代內存區域是空閑的,使得內存使用率不高。

針對這種情況,復制算法的垃圾收集器可以有優化的地方。而優化的基礎,就是分代收集。據IBM的研究,98%的對象都是朝生夕死的,因此并不需要1:1的比例分成from和to區域。對于HotSpot虛擬機,從一開始就使用了優化版的復制算法,它將內存區域分成3部分,一個較大的Eden區,兩個較小的Survivor區域。每個新創建的對象都分配到Eden區,當垃圾回收時,總是回收Eden區和一個Survivor區,并將這兩個區域中存活的對象都復制到剩下的那個Survivor區域里。收集完成后,之前的Survivor區和Eden區被清空,之前的Survivor區就變為下次gc時對象要拷貝到的區域了。

Eden和2個Survivor的比例在HotSpot中默認是8:1:1。因此只有10%的內存會被空閑出來。另外前面說到的98%的對象可以被回收并不是絕對的,因此可能出現回收后Survivor區容納不下存活對象的情況,這時分代收集的好處就體現出來了。由于有老生代的存在,多出來的存活對象可以被提前提升到老生代中,這個機制被稱作內存擔保。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容