Java 教程

Java 控制語句

面向物件程式設計

Java 內建類

Java 檔案處理

Java 錯誤和異常

Java 多執行緒

Java 同步

Java 網路程式設計

Java 集合

Java 介面

Java 資料結構

Java 集合演算法

高階 Java

Java 雜項

Java API 和框架

Java 類引用

Java 有用資源

Java - 垃圾回收



一個Java物件的生命週期由JVM管理。一旦程式設計師建立了一個物件,我們就無需擔心它的其餘生命週期。JVM 將自動查詢不再使用的物件,並從堆中回收它們的記憶體。

什麼是 Java 垃圾回收?

垃圾回收是 JVM 執行的一項主要操作,對其進行調整以滿足我們的需求可以極大地提高應用程式的效能。現代 JVM 提供了各種各樣的垃圾回收演算法。我們需要了解應用程式的需求才能決定使用哪種演算法。

您不能像在 C 和 C++ 等非 GC 語言中那樣以程式設計方式釋放 Java 中的物件。因此,您在 Java 中不會有懸空引用。但是,您可能有空引用(引用指向 JVM 永遠不會儲存物件的記憶體區域)。每當使用空引用時,JVM 都會丟擲 NullPointerException。

請注意,由於GC,在 Java 程式中很少發現記憶體洩漏,但它們確實會發生。我們將在本章末尾建立一個記憶體洩漏。

垃圾收集器的型別

現代 JVM 使用以下 GC

  • 序列收集器
  • 吞吐量收集器
  • CMS 收集器
  • G1 收集器

上述每種演算法都執行相同的任務 - 查詢物件,這些物件不再使用,並回收它們在堆中佔用的記憶體。一種簡單的方法是計算每個物件具有的引用數,並在引用數變為 0 時將其釋放(這也稱為引用計數)。為什麼這是天真的?考慮一個迴圈連結串列。它的每個節點都會對其有一個引用,但是整個物件並沒有從任何地方被引用,理想情況下應該被釋放。

記憶體合併

JVM 不僅釋放記憶體,還會將小的記憶體塊合併成更大的記憶體塊。這樣做是為了防止記憶體碎片。

簡單來說,典型的 GC 演算法執行以下活動:

  • 查詢未使用的物件
  • 釋放它們在堆中佔用的記憶體
  • 合併碎片

GC 在執行時必須停止應用程式執行緒。這是因為它在執行時會移動物件,因此無法使用這些物件。此類停止稱為“停止世界”暫停,我們的目標是在調整 GC 時最大限度地減少這些暫停的頻率和持續時間。

下面顯示了一個記憶體合併的簡單演示

陰影部分是需要釋放的物件。即使在回收所有空間後,我們也只能分配最大大小為 75Kb 的物件。即使我們有 200Kb 的可用空間,如下所示

垃圾回收中的代

大多數 JVM 將堆分為三代:年輕代 (YG)、老年代 (OG) 和永久代(也稱為終身代)

我們來看一個簡單的例子。Java 中的 String 類是不可變的。這意味著每次您需要更改 String 物件的內容時,都必須建立一個全新的物件。假設您在迴圈中對字串進行了 1000 次更改,如下面的程式碼所示:

String str = "G11 GC";

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

在每次迴圈中,我們都會建立一個新的字串物件,而上一次迭代建立的字串就變得無用了(也就是說,沒有任何引用指向它)。該物件的生存期只有一次迭代——它們很快就會被垃圾收集器 (GC) 回收。這種短暫存在的物件儲存在堆的年輕代區域中。從年輕代收集物件的程序稱為次要垃圾回收,它總是會導致“stop-the-world”暫停。

次要垃圾回收

隨著年輕代被填滿,GC 會進行次要垃圾回收。已死亡的物件會被丟棄,而存活的物件會被移動到老年代。在此過程中,應用程式執行緒會停止。

在這裡,我們可以看到這種分代設計帶來的優勢。年輕代只是堆的一小部分,很快就滿了。但是處理它所需的時間遠遠小於處理整個堆所需的時間。因此,在這種情況下,“stop-the-world”暫停時間要短得多,儘管頻率更高。我們應該始終追求更短的暫停時間,即使它們可能更頻繁。

完全垃圾回收

年輕代被分為兩個空間——**伊甸園區 (eden)** 和**倖存者區 (survivor space)**。在伊甸園區收集期間倖存下來的物件會被移動到倖存者區,而那些在倖存者區倖存下來的物件會被移動到老年代。年輕代在收集時會被壓縮。

隨著物件被移動到老年代,它最終會被填滿,必須進行收集和壓縮。不同的演算法採用不同的方法來實現這一點。有些演算法會停止應用程式執行緒(這會導致長時間的“stop-the-world”暫停,因為老年代與年輕代相比相當大),而有些演算法則在應用程式執行緒繼續執行的同時併發地執行此操作。這個過程稱為完全 GC (full GC)。CMS 和 G1 就是兩種這樣的垃圾收集器。

垃圾收集器調優

我們也可以根據需要調整垃圾收集器。以下是可以根據情況配置的區域

  • 堆大小分配
  • 分代大小分配
  • 永久代和元空間配置

讓我們在瞭解它們的影響的同時詳細瞭解每一個方面。我們還將討論基於可用記憶體、CPU 配置和其他相關因素的建議。

堆大小分配

堆大小是 Java 應用程式效能的一個重要因素。如果它太小,它將頻繁填滿,結果將不得不被 GC 頻繁收集。另一方面,如果我們只是增加堆的大小,雖然它需要收集的頻率較低,但暫停的長度會增加。

此外,增加堆大小會對底層作業系統造成嚴重的懲罰。透過分頁,作業系統使我們的應用程式程式看到的記憶體比實際可用的記憶體多得多。作業系統透過使用磁碟上的某些交換空間來管理這一點,將程式的非活動部分複製到其中。當需要這些部分時,作業系統會將它們從磁碟複製回記憶體。

讓我們假設一臺機器有 8G 記憶體,而 JVM 看到了 16G 虛擬記憶體,JVM 將不知道實際上系統上只有 8G 可用。它只會向作業系統請求 16G,一旦獲得該記憶體,它將繼續使用它。作業系統將不得不大量地交換資料進出,這對系統來說是一個巨大的效能損失。

然後是這種虛擬記憶體完全 GC 期間發生的暫停。由於 GC 將對整個堆進行收集和壓縮,它將不得不等待很長時間才能將虛擬記憶體從磁碟交換出去。如果是併發收集器,後臺執行緒將不得不等待很長時間才能將資料從交換空間複製到記憶體。

因此,這裡就出現瞭如何確定最佳堆大小的問題。第一條規則是永遠不要向作業系統請求超過實際存在的記憶體。這將完全防止頻繁交換的問題。如果機器安裝並執行多個 JVM,則所有 JVM 的總記憶體請求小於系統中實際存在的RAM

您可以使用兩個標誌控制 JVM 的記憶體請求大小:

  • -XmsN − 控制請求的初始記憶體。
  • -XmxN − 控制可以請求的最大記憶體。

這兩個標誌的預設值取決於底層作業系統。例如,對於在 MacOS 上執行的 64 位 JVM,-XmsN = 64M,-XmxN = 最小 1G 或總物理記憶體的 1/4。

請注意,JVM 可以自動在這兩個值之間進行調整。例如,如果它注意到 GC 發生得太頻繁,它將不斷增加記憶體大小,只要它低於 -XmxN 並滿足所需的效能目標。

如果您確切知道您的應用程式需要多少記憶體,那麼您可以設定 -XmsN = -XmxN。在這種情況下,JVM 不需要計算堆的“最佳”值,因此 GC 過程變得更高效。

分代大小分配

您可以決定要為年輕代分配多少堆記憶體,以及要為老年代分配多少堆記憶體。這兩個值都會以以下方式影響應用程式的效能。

如果年輕代的大小非常大,那麼它將被收集的頻率較低。這將導致晉升到老年代的物件數量較少。另一方面,如果您過分增加老年代的大小,那麼收集和壓縮它將花費太多時間,這將導致長時間的 STW 暫停。因此,使用者必須在這兩個值之間找到平衡。

以下是您可以用來設定這些值的標誌:

  • -XX:NewRatio=N:年輕代與老年代的比率(預設值為 2)
  • -XX:NewSize=N:年輕代的初始大小
  • -XX:MaxNewSize=N:年輕代的最大大小
  • -XmnN:使用此標誌將 NewSize 和 MaxNewSize 設定為相同的值

年輕代的初始大小由 NewRatio 的值根據以下公式確定:

(total heap size) / (newRatio + 1)

由於 newRatio 的初始值為 2,上述公式得出年輕代的初始值為總堆大小的 1/3。您可以始終透過顯式指定年輕代的大小來覆蓋此值,使用 NewSize 標誌。此標誌沒有任何預設值,如果未顯式設定,年輕代的大小將繼續使用上述公式計算。

永久代和元空間配置

永久代和元空間是 JVM 保留類元資料的堆區域。在 Java 7 中,該空間稱為“永久代”,在 Java 8 中,它被稱為“元空間”。編譯器和執行時使用此資訊。
您可以使用以下標誌控制永久代的大小:-XX:PermSize=N-XX:MaxPermSize=N。元空間的大小可以使用:-XX:MetaspaceSize=N-XX:MaxMetaspaceSize=N 控制。

未設定標誌值時,永久代和元空間的管理方式存在一些差異。預設情況下,兩者都有一個預設的初始大小。但是,雖然元空間可以佔用所需的大量堆記憶體,但永久代最多隻能佔用預設初始值。例如,64 位 JVM 的最大永久代大小為 82M。

請注意,除非指定不佔用,否則元空間可以佔用無限量的記憶體,因此可能會出現記憶體不足錯誤。每當這些區域調整大小時,都會發生完全 GC。因此,在啟動過程中,如果有很多類正在載入,元空間可能會不斷調整大小,每次都會導致完全 GC。因此,對於大型應用程式來說,如果初始元空間大小太小,啟動時間會很長。最好增加初始大小,因為它可以減少啟動時間。

儘管永久代和元空間儲存類元資料,但它不是永久的,並且像物件一樣,空間會被 GC 回收。這通常是伺服器應用程式的情況。每當您向伺服器進行新的部署時,都必須清理舊的元資料,因為新的類載入器現在需要空間。此空間由 GC 釋放。

廣告