Linux中國

JVM 垃圾回收的工作原理

對於程序員來說,掌握 Java 的內存管理機制並不是必須的,但它能夠幫助你更好地理解 JVM 是如何處理程序中的變數和類實例的。

Java 之所以能夠如此流行,自動 垃圾回收 Garbage Collection (GC)功不可沒,它也是 Java 最重要的幾個特性之一。在這篇文章中,我將說明為什麼垃圾回收如此重要。本文的主要內容為:自動的分代垃圾回收、JVM 劃分內存的依據,以及 JVM 垃圾回收的工作原理。

Java 內存分配

Java 程序的內存空間被劃分為以下四個區域:

  1. 堆區 Heap :對象實例就是在這個區域分配的。不過,當我們聲明一個對象時,堆中不會發生任何內存分配,只是在棧中創建了一個對象的引用而已。
  2. 棧區 Stack :方法、局部變數和類的實例變數就是在這個區域分配的。
  3. 代碼區 Code :這個區域存放了程序的位元組碼。
  4. 靜態區 Static :這個區域存放了程序的靜態數據和靜態方法。

什麼是自動垃圾回收?

自動垃圾回收是這樣一個過程:首先,堆中的所有對象會被分類為「被引用的」和「未被引用的」;接著,「未被引用的對象」就會被做上標記,以待之後刪除。其中,「被引用的對象」是指程序中的某一部分仍在使用的對象,「未被引用的對象」是指目前沒有正在被使用的對象。

許多編程語言,例如 C 和 C++,都需要程序員手動管理內存的分配和釋放。在 Java 中,這一過程是通過垃圾回收機制來自動完成的(儘管你也可以在代碼中調用 system.gc(); 來手動觸發垃圾回收)。

垃圾回收的基本步驟如下:

1、標記已使用和未使用的對象

在這一步驟中,已使用和未使用的對象會被分別做上標記。這是一個及其耗時的過程,因為需要掃描內存中的所有對象,才能夠確定它們是否正在被使用。

標記已使用和未使用的對象

2、掃描/刪除對象

有兩種不同的掃描和刪除演算法:

簡單刪除(標記清除):它的過程很簡單,我們只需要刪除未被引用的對象即可。但是,後續給新對象分配內存就會變得很困難了,因為可用空間被分割成了一塊塊碎片。

標記清除的過程

刪除壓縮(標記整理):除了會刪除未被引用的對象,我們還會壓縮被引用的對象(未被刪除的對象)。這樣以來,新對象的內存分配就相對容易了,並且內存分配的效率也有了提升。

標記整理的過程

什麼是分代垃圾回收,為什麼需要它?

正如我們在「掃描刪除」模型中所看到的,一旦對象不斷增長,我們就很難掃描所有未使用的對象以回收內存。不過,有一項實驗性研究指出,在程序執行期間創建的大多數對象,它們的存活時間都很短。

既然大多數對象的存活時間都很短,那麼我們就可以利用這個事實,從而提升垃圾回收的效率。該怎麼做呢?首先,JVM 將內存劃分為不同的「代」。接著,它將所有的對象都分類到這些內存「代」中,然後對這些「代」分別執行垃圾回收。這就是「分代垃圾回收」。

堆內存的「代」和分代垃圾回收過程

為了提升垃圾回收中的「標記清除」的效率,JVM 將對內存劃分成以下三個「代」:

  • 新生代 Young Generation
  • 老年代 Old Generation
  • 永久代 Permanent Generation

Hotspot 堆內存結構

下面我將介紹每個「代」及其主要特徵。

新生代

所有創建不久的對象都存放在這裡。新生代被進一步分為以下兩個區域:

  1. 伊甸區 Eden :所有新創建的對象都在此處分配內存。
  2. 倖存者區 Survivor ,分為 S0 和 S1:經歷過一次垃圾回收後,仍然存活的對象會被移動到兩個倖存者區中的一個。

對象分配

在新生代發生的分代垃圾回收被稱為 「 次要回收 Minor GC 」(LCTT 譯註:也稱為「 新生代回收 Young GC 」)。Minor GC 過程中的每個階段都是「 停止世界 Stop The World 」(STW)的,這會導致其他應用程序暫停運行,直到垃圾回收結束。這也是次要回收更快的原因。

一句話總結:伊甸區存放了所有新創建的對象,當它的可用空間被耗盡,第一次垃圾回收就會被觸發。

填充伊甸區

次要回收:在該垃圾回收過程中,所有存活和死亡的對象都會被做上標記。其中,存活對象會被移動到 S0 倖存者區。當所有存活對象都被移動到了 S0,未被引用的對象就會被刪除。

拷貝被引用的對象

S0 中的對象年齡為 1,因為它們挺過了一次次要回收。此時,伊甸區和 S1 都是空的。

每當完成清理後,伊甸區就會再次接受新的存活對象。隨著時間的推移,伊甸區和 S0 中的某些對象被宣判死亡(不再被引用),並且伊甸區的可用空間也再次耗盡(填滿了),那麼次要回收 又將再次被觸發。

對象年齡增長

這一次,伊甸區和 S0 中的死亡和存活的對象會被做上標記。其中,伊甸區的存活對象會被移動到 S1,並且年齡增加至 1。S0 中的存活對象也會被移動到 S1,並且年齡增加至 2(因為它們挺過了兩次次要回收)。此時,伊甸區和 S0 又是空的了。每次次要回收之後,伊甸區和兩個倖存者區中的一個都會是空的。

新對象總是在伊甸區被創建,周而復始。當下一次垃圾回收發生時,伊甸區和 S1 都會被清理,它們中的存活對象會被移動到 S0 區。每次次要回收之後,這兩個倖存者區(S0 和 S1)就會交換一次。

額外年齡增長

這個過程會一直進行下去,直到某個存活對象的年齡達到了某個閾值,然後它就會被移動到一個叫做「老年代」的地方,這是通過一個叫做「晉陞」的過程來完成的。

使用 -Xmn 選項可以設置新生代的大小。

老年代

這個區域存放著那些挺過了許多次次要回收,並且達到了某個年齡閾值的對象。

晉陞

在上面這個示例圖表中,晉陞的年齡閾值為 8。在老年代發生的垃圾回收被稱為 「 主要回收 Major GC 」。(LCTT 譯註:也被稱為「 全回收 Full GC 」)

使用 -Xms-Xmx 選項可以分別設置堆內存大小的初始值和最大值。(LCTT 譯註:結合上面的 -Xmn 選項,就可以間接設置老年代的大小了。)

永久代

永久代存放著一些元數據,它們與應用程序、Java 標準環境以及 JVM 自用的庫類及其方法相關。JVM 會在運行時,用到了什麼類和方法,就會填充相應的數據。當 JVM 發現有未使用的類,就會卸載或是回收它們,從而為正在使用的類騰出空間。

使用 -XX:PermGen-XX:MaxPerGen 選項可以分別設置永久代大小的初始值和最大值。

元空間

Java 8 引入了 元空間 Metaspace ,並用它替換了永久代。這麼做的好處是自動調整大小,避免了 內存不足 OutOfMemory (OOM)錯誤。

總結

本文討論了各種不同的 JVM 內存「代」,以及它們是如何在分代垃圾回收演算法中起作用的。對於程序員來說,掌握 Java 的內存管理機制並不是必須的,但它能夠幫助你更好地理解 JVM 處理程序中的變數和類實例的方式。這種理解使你能夠規劃和排除代碼故障,並理解特定平台固有的潛在限制。

正文配圖來自:Jayashree Huttanagoudar,CC BY-SA 4.0

via: https://opensource.com/article/22/6/garbage-collection-java-virtual-machine

作者:Jayashree Huttanagoudar 選題:lkxed 譯者:lkxed 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國