閒談JVM(六):JVM垃圾回收概述

前言

深入理解JVM虛擬機:(二)垃圾收集器概述

閒談JVM(三):淺析本地元空間參數配置

閒談JVM(二):淺析新老生代參數配置

閒談JVM(一):淺析JVM Heap參數配置

本篇,我們聊聊JVM的垃圾回收機制。

JVM垃圾回收機制

衆所周知,Java語言的核心特性之一,就是JVM的垃圾回收(GC)機制,可以讓開發者無需關注程序的內存分配問題,將這部分瑣碎且容易出錯的操作交給JVM來進行處理。

但是這並不代表着作爲開發者不需要去了解JVM的垃圾收集機制,當垃圾收集是主要瓶頸時,瞭解此垃圾收集實現的某些方面會非常有用。

何爲垃圾

那在JVM中,哪些對象可以被標記爲「垃圾」?

官方文檔對此的定義是:

An object is considered garbage when it can no longer be reached from any pointer in the running program.

在程序運行過程中,沒有任何指針指向的對象,可以被認爲是「垃圾對象」。

最簡單的垃圾回收算法會遍歷每個可訪問的對象,剩下的任何對象都被視爲垃圾。

但是這種方法花費的時間與活動對象的數量成正比,這對於維護大量活動數據的大型應用程序是不可行的。

Java虛擬機結合了許多不同的垃圾收集算法,這些算法使用分代收集進行組合。

爲何分代管理

對象生命週期的典型分佈

在Java虛擬機中,對象的生命週期大致可以如上圖所示,x軸是對象壽命,以分配的字節爲單位。y軸上的字節數是具有相應生存期的對象中的總字節數。

絕大多數的對象的生命週期是非常短暫的,只有少部分的對象生命週期非常的長,通常有一些在初始化時分配的對象,這些對象一直存在,直到進程退出。

由此,爲了針對這種情況進行優化,Java虛擬機採用了分代管理的策略,即將整個堆區切分成幾個存儲着不同年齡對象的內存池,將其分爲了新生代老生代本地元空間(永生代/方法區)

JVM內存模型

當某個分代內存不足時,垃圾回收會在該分代中進行,而會影響其他分代。

絕大多數對象分配在新生代中,並且大多數對象在那裏死亡。因此,新生代的垃圾回收會頻繁進行,經過多次垃圾回收仍存活的對象,可以進行「晉升」,進入到老年代當中。當然也有一部分大對象,其大小超過指定的閾值時,會直接被分配在老生代中,當老生代內存不足時,會進行老生代的垃圾回收,周而復始,保持整個堆區的內存可用性。

生代內存分配

對於新老生代的內存大小,我們可以通過參數進行配置,但是這部分物理內存真正全部分配給JVM了麼?

我們來看一下官方文檔的解釋:

At initialization, a maximum address space is virtually reserved but not allocated to physical memory unless it is needed. The complete address space reserved for object memory can be divided into the young and tenured generations.

在JVM初始化階段時,最大的地址空間實際上是保留的,除非需要,否則不會分配給物理內存。

這部分內存空間會被保留,等待分配至新老生代。

也就是說,當我們使用-Xmn128m 參數指定了新生代內存大小,但JVM初始化階段,並未真的使用了OS這麼大的內存,而是預先佔用,在真正使用的時候,才進行分配。

生代內存分配

對於新生代內存,JVM又將其分爲三部分,eden、survivor01、survivor02,大多數對象初始化階段會被分配在eden區域,而兩個survivor區域的其中一個會一直保持爲空,用於GC收集階段的對象整理。

劃分出新生代的另一個好處是某種程度上解決了碎片化問題,或者說將最壞的情況推遲了。

那些存活時間短的小對象本來可能產生碎片化問題,但都在新生代的垃圾收集中被清理了。

由於存活時間長的對象被移到老年代時被更緊湊的分配空間,老年代也更加緊湊了。

隨着時間推移(如果你的應用運行時間足夠長),老年代也會產生碎片化,這時需要運行一次或是幾次完全垃圾收集,同時JVM也有可能拋出內存溢出錯誤。

但是劃分出新生代推遲了出現最壞情況的時間,這對於很多應用程序來說已經足夠了。對於多數應用程序而言,它的確降低了stop-the-world垃圾收集的頻率和內存溢出錯誤的機會。

由此可以看出堆區內存分代管理的好處,可以將各生代的內存根據實際情況更加合理的利用與調配。

吞吐量 VS 暫停時間

對於大多數的應用領域,評估一個垃圾收集(GC)算法如何根據如下兩個標準:

  • 吞吐量越高算法越好
  • 暫停時間越短算法越好

首先讓我們來明確垃圾收集(GC)中的兩個術語:吞吐量(throughput)和暫停時間(pause times)。

吞吐量(throughput)

JVM在專門的線程(GC threads)中執行GC。只要GC線程是活動的,它們將與應用程序線程(application threads)爭用當前可用CPU的時鐘週期。

簡單點來說,吞吐量是指應用程序線程用時佔程序總用時的比例。例如,吞吐量99/100意味着100秒的程序執行時間應用程序線程運行了99秒, 而在這一時間段內GC線程只運行了1秒。

暫停時間(pause times)

暫停時間是指一個時間段內應用程序線程讓與GC線程執行而完全暫停。 例如,GC期間100毫秒的暫停時間意味着在這100毫秒期間內沒有應用程序線程是活動的。

如果說一個正在運行的應用程序有100毫秒的「平均暫停時間」,那麼就是說該應用程序所有的暫停時間平均長度爲100毫秒。同樣,100毫秒的「最大暫停時間」是指該應用程序所有的暫停時間最大不超過100毫秒。

高吞吐量最好是指這會讓應用程序的最終用戶感覺只有應用程序線程在做「生產性」工作。

直覺上,吞吐量越高程序運行越快。 低暫停時間最好因爲從最終用戶的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。 這取決於應用程序的類型,有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗。

因此,具有低的最大暫停時間是非常重要的,特別是對於一個交互式應用程序。

垃圾收集器種類

在JVM中,由於分代管理的存在,垃圾收集器可以分爲兩大類,新生代收集器與老生代收集器,下面我們來進行簡單的介紹。

新生代收集器

新生代的垃圾收集器,主要有三種,分別如下:

1、Serial 收集器,它是一個單線程的收集器,但它的單線程的意義並不僅僅說明它只會是使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。

2、ParNew 收集器,ParNew 收集器其實就是 Serial 收集器的多線程版本。ParNew 最重要的一點,是唯一的可以與老生代的CMS 收集器配合使用的新生代收集器,可以通過參數-XX:+UseParNewGC進行指定。

3、Parallel Scavenge 收集器,它與其他收集器的不同之處在於:它的關注點與其他收集器不同。CMS 等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量( Throughput)。

老生代收集器

老生代的垃圾收集器,主要分爲四種,分別如下:

1、Serial Old 收集器,Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用 「標記-整理」 算法。一般情況下,不會使用。

2、Parallel old 收集器,Parallel Scavenge 收集器的老年代版本,使用多線程和 「標記-整理」 算法。

3、CMS 收集器,以獲取最短回收停頓時間爲目標,目前較爲推薦的GC 收集器,多數應用於互聯網站或者B/S系統的服務器端上。

4、G1 收集器,Java 9以後的默認收集器,當前最炙手可熱的GC 收集器,可以說兼顧了性能與時間的GC收集器。

在Java8中,默認的GC收集器採用了Parallel GC,也可以通過參數-XX:+UseParallelGC進行指定,來看一下Oracle官方的說法:

The parallel collector (also referred to here as the throughput collector) is a generational collector similar to the serial collector; the primary difference is that multiple threads are used to speed up garbage collection. The parallel collector is enabled with the command-line option -XX:+UseParallelGC. By default, with this option, both minor and major collections are executed in parallel to further reduce garbage collection overhead.

The parallel collector is selected by default on server-class machines. In addition, the parallel collector uses a method of automatic tuning that allows you to specify specific behaviors instead of generation sizes and other low-level tuning details. You can specify maximum garbage collection pause time, throughput, and footprint (heap size).

Partial GC VS Full GC

前面的介紹中,我們提到了新生代的GC與老生代的GC,但事實上,針對HotSpot VM的實現,它裏面的GC其實準確分類只有兩大種:

Partial GC:並不收集整個GC堆的模式。

  • Young GC:只收集新生代的GC
  • Old GC:只收集老生代的GC。只有CMS的concurrent collection是這個模式
  • Mixed GC:收集整個新生代以及部分老生代的GC。只有G1有這個模式

Full GC:收集整個堆,包括新生代、老生代、永生代(本地元空間)等所有部分的模式。

關於新老生代的GC觸發時機,我們前面已經提及,當所屬生代的空間不足時,會觸發所在生代的GC操作,

但Full GC的觸發時機則有所不同,其可能的觸發時機如下:

1、當需要在永生代(本地元空間)分配空間但已經沒有足夠空間時;

2、在代碼中執行System.gc(),或者執行JVM命令jmap -histo:live <pid>

3、當準備要觸發一次Young GC時,如果發現統計數據說之前新生代的平均晉升大小比目前老生代剩餘的空間大,則不會觸發Young GC而是轉爲觸發Full GC(因爲HotSpot VM的GC裏,除了CMS的concurrent collection之外,其它能收集老生代的GC都會同時收集整個GC堆,包括新生代,所以不需要事先觸發一次單獨的young GC)

Full GC的執行代價會非常的高,它會對整個堆區進行整理收集,並進行Stop-the-world,暫停全部用戶線程,直到Full GC執行結束。

結語

本篇我們對JVM的垃圾回收機制進行了簡單的概述,在下一篇中,將會對具體的垃圾回收器進行詳細的分析,敬請期待。