天天看點

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

本文是在讀完深入了解Java虛拟機(周志明著)後的總結,有很多部分借鑒了原書的說法,如果想深入了解這些内容,推薦看原書

JVM垃圾回收政策

垃圾回收主要包括确定垃圾和回收垃圾兩步,JVM采用可達性分析算法分析哪些是廢棄對象需要回收,然後采用GC算法進行垃圾清理(GC算法)。由于堆記憶體的使用情況影響了垃圾回收,是以JVM将堆記憶體劃分成了幾個區域,不同區域采用不同的垃圾收集方式

1.确定對象是否可被清除

1)引用計數算法

引用計數算法的實作方式是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;計數器為0的對象就是不可能再被使用的

引用計數算法的優點是實作簡單,判定效率高。缺點是沒有辦法處理循環引用問題

循環引用是指兩個對象互相引用了對方,這樣即使他們不再被其他對象調用,已經成為需要被清除的垃圾時,他們的引用計數器值均為1而不是0,進而無法被判定為垃圾并進行清除。Java沒有采用這種算法确定垃圾對象

2)可達性分析算法

可達性分析(Reachability Analysis)算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。

如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,是以它們将會被判定為是可回收的對象。可達性分析算法沒有循環引用問題,他是JVM采用的判斷對象是否可被清除的算法

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

2.JVM堆記憶體劃分

JVM将運作時記憶體分成了幾個區域,其中堆區由于其存儲的是共享資料(對象),是以需要不斷地進行記憶體配置設定和垃圾回收,而其他區域則很少或不用進行這些操作

為了更好地配置設定和回收堆記憶體,JVM将堆記憶體劃分為幾個部分,便于進行記憶體配置設定和垃圾回收。劃分的各部分記憶體各有不同的用途,采用的垃圾收集器也不相同。而且,各部分記憶體的大小和采用的垃圾收集器都可以通過參數進行調整,以達到最優的運作狀态

首先堆記憶體劃分為老年代和新生代,新生代存儲剛生成的對象,老年代存儲已經生成并使用了一段時間的對象。新生代又可以分為兩個區域,分别為Eden區和Survivor區,其中Survivor區又分為三個部分,一個Eden區和兩個Survivor區,兩個Survivor區分别名為From Survivor和To Survivor

各個區域都有預設的大小(可以通過參數進行調節),其中老年代占整個堆記憶體的2/3,新生代占1/3,Eden區占新生代的8/10,From Survivor和To Survivor各占1/10

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

JVM中的垃圾收集器

Java虛拟機規範中對垃圾收集器應該如何實作并沒有任何規定,是以不同的廠商、不同版本的虛拟機所提供的垃圾收集器有很大差别,各個收集器有不同的工作原理工作方式,适合不同的工作場景,具體場景下使用哪種收集器需要根據情況選擇

新生代和老年代的垃圾收集情況差異較大(新生代中産生的垃圾遠多于老年代),是以大部分收集器工作于新生代或老年代中的一個,但是G1收集器可以同時勝任這兩個區域。下圖展示了垃圾收集器的适用區域,例如Serial收集器适合在新生代工作。由于一種收集器一般隻勝任一個區域,是以大多情況下需要兩款收集器配合工作,一款收集老年代,一款收集新生代。下圖中連起來的兩個收集器可以配合工作

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

Serial收集器

Serial收集器是最基本、發展曆史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛拟機新生代收集的唯一選擇。它是一個單線程的收集器,隻會使用一個CPU開啟一條收集線程去完成垃圾收集工作。還有更重要的一點是,它在工作時必須關閉其他正在工作的使用者線程,直到它收集結束,這種工作方式被稱作:“Stop The World”

相對于之後的CMS等收集器可以在不關閉使用者線程的情況下運作垃圾回收線程,“Stop The World”的方式不适合處理待處理如虛拟機Server模式下那種垃圾較多的場景,因為這種情況下使用者線程就得停頓不少時間,而如果待處理垃圾不多,如在虛拟機Client模式下,“Stop The World”可以在極短時間内完成垃圾回收,不影響使用者體驗

關閉使用者線程進行垃圾回收的效率要比使用者線程和垃圾處理線程并行更高,并行的垃圾處理方式也有更高的執行頻率(即因為回收效率低是以要頻繁進行垃圾回收),會在背景頻繁開啟垃圾回收線程,這會消耗CPU資源,使得程式運作速度減慢,不過為了能夠不讓程式停頓,這點犧牲是值得的,Serial收集器之後的很多收集器,都是圍繞着如何縮短使用者停頓的時間設計的

Serial收集器的優點是簡單而高效,雖然已經有很長的曆史,但它還是虛拟機運作在Client模式下的預設新生代收集器

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

ParNew收集器

ParNew收集器和Serial收集器非常相似,唯一的不同就是ParNew收集器使用多線程進行垃圾收集,在JDK1.5中它經常和CMS收集器配合使用,在新生代工作

由于存線上程互動的開銷,ParNew收集器在單CPU的環境中工作效率不如Serial收集器,但是随着可以使用的CPU的數量的增加,它的多線程優勢就漸漸展現出來。它預設開啟的收集線程數與CPU的數量相同,在CPU非常多的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它使用複制算法,并行多線程收集。Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時使用者線程的停頓時間,而Parallel Scavenge收集器的目标則是達到一個可控制的吞吐量(Throughput)

吞吐量是CPU用于運作使用者代碼的時間與CPU總消耗時間的比值,即吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間),虛拟機總共運作了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時間越短就越适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可以高效率地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。

Serial Old收集器

Serial Old是收集器Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“标記-整理”算法。這個收集器和Serial收集器一樣,其主要用于Client模式下的虛拟機

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“标記-整理”算法。Parallel Old收集器的主要用途是和Parallel Scavenge收集器配合,在注重吞吐量以及CPU資源敏感的場合使用

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求

CMS收集器是基于“标記—清除”算法實作的,它的運作過程相對于前面幾種收集器來說更複雜一些,整個過程分為4個步驟,包括:

  • 初始标記(CMS initial mark)
  • 并發标記(CMS concurrent mark)
  • 重新标記(CMS remark)
  • 并發清除(CMS concurrent sweep)

其中,初始标記、重新标記這兩個步驟仍然需要“Stop The World”。初始标記僅僅隻是标記一下GC Roots能直接關聯到的對象,速度很快,并發标記階段就是進行GC RootsTracing的過程,而重新标記階段則是為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短。由于整個過程中耗時最長的并發标記和并發清除過程收集器線程都可以與使用者線程一起工作,是以,從總體上來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器

CMS收集器雖然做到了并發執行,低停頓,但是為了達到這兩個特性它犧牲了一些性能,CMS收集器有幾個明顯的缺點:

第一點是CMS收集器對CPU資源非常敏感,它采用并發收集,占用了一部分線程(或者說CPU資源)而導緻應用程式變慢,系統總吞吐量會降低。在CPU不多時,這種速度降低就很明顯

第二點是CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導緻另一次Full GC的産生。CMS并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS無法在當次收集中處理掉,這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段使用者線程還需要運作,那也就還需要預留有足夠的記憶體空間給使用者線程使用,是以CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程式運作使用

第三點是由于CMS是一款基于“标記—清除”算法實作的收集器,使用這種GC算法會有大量空間碎片産生。空間碎片過多時,将會給大對象配置設定帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來配置設定目前對象不得不提前觸發一次Full GC,頻繁地觸發Full GC會很大地降低效率

G1收集器

G1(Garbage-First)收集器是CMS的改進型和替代者,GI收集器和CMS收集器一樣,采用多線程分代收集,但是GC算法沒有采用和CMS一樣的“标記—清除”算法,而是采用了“标記—整理”算法,避免了收集後産生空間碎片的情況。同時G1收集器可以設定停頓時間,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒。G1收集器另一個優于CMS收集器的方面是它除了可以工作于老年代,還可以在新生代工作

G1收集器能有以上諸多優點,是因為它的回收方式很特别,G1收集器将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region(不需要連續)的集合。每個Region都有一個與之對應的Remembered Set,虛拟機發現程式在對Reference類型的資料進行寫操作時,會産生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set之中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏

JVM垃圾回收政策與垃圾收集器JVM垃圾回收政策JVM中的垃圾收集器