JVM 垃圾回收的工作原理
對於程式設計師來說,掌握 Java 的記憶體管理機制並不是必須的,但它能夠幫助你更好地理解 JVM 是如何處理程式中的變數和類例項的。
Java 之所以能夠如此流行,自動 垃圾回收Garbage Collection(GC)功不可沒,它也是 Java 最重要的幾個特性之一。在這篇文章中,我將說明為什麼垃圾回收如此重要。本文的主要內容為:自動的分代垃圾回收、JVM 劃分記憶體的依據,以及 JVM 垃圾回收的工作原理。
Java 記憶體分配
Java 程式的記憶體空間被劃分為以下四個區域:
- 堆區Heap:物件例項就是在這個區域分配的。不過,當我們宣告一個物件時,堆中不會發生任何記憶體分配,只是在棧中建立了一個物件的引用而已。
- 棧區Stack:方法、區域性變數和類的例項變數就是在這個區域分配的。
- 程式碼區Code:這個區域存放了程式的位元組碼。
- 靜態區Static:這個區域存放了程式的靜態資料和靜態方法。
什麼是自動垃圾回收?
自動垃圾回收是這樣一個過程:首先,堆中的所有物件會被分類為“被引用的”和“未被引用的”;接著,“未被引用的物件”就會被做上標記,以待之後刪除。其中,“被引用的物件”是指程式中的某一部分仍在使用的物件,“未被引用的物件”是指目前沒有正在被使用的物件。
許多程式語言,例如 C 和 C++,都需要程式設計師手動管理記憶體的分配和釋放。在 Java 中,這一過程是通過垃圾回收機制來自動完成的(儘管你也可以在程式碼中呼叫 system.gc(); 來手動觸發垃圾回收)。
垃圾回收的基本步驟如下:
1、標記已使用和未使用的物件
在這一步驟中,已使用和未使用的物件會被分別做上標記。這是一個及其耗時的過程,因為需要掃描記憶體中的所有物件,才能夠確定它們是否正在被使用。
標記已使用和未使用的物件
2、掃描/刪除物件
有兩種不同的掃描和刪除演算法:
簡單刪除(標記清除):它的過程很簡單,我們只需要刪除未被引用的物件即可。但是,後續給新物件分配記憶體就會變得很困難了,因為可用空間被分割成了一塊塊碎片。
標記清除的過程
刪除壓縮(標記整理):除了會刪除未被引用的物件,我們還會壓縮被引用的物件(未被刪除的物件)。這樣以來,新物件的記憶體分配就相對容易了,並且記憶體分配的效率也有了提升。
標記整理的過程
什麼是分代垃圾回收,為什麼需要它?
正如我們在“掃描刪除”模型中所看到的,一旦物件不斷增長,我們就很難掃描所有未使用的物件以回收記憶體。不過,有一項實驗性研究指出,在程式執行期間建立的大多數物件,它們的存活時間都很短。
既然大多數物件的存活時間都很短,那麼我們就可以利用這個事實,從而提升垃圾回收的效率。該怎麼做呢?首先,JVM 將記憶體劃分為不同的“代”。接著,它將所有的物件都分類到這些記憶體“代”中,然後對這些“代”分別執行垃圾回收。這就是“分代垃圾回收”。
堆記憶體的“代”和分代垃圾回收過程
為了提升垃圾回收中的“標記清除”的效率,JVM 將對記憶體劃分成以下三個“代”:
- 新生代Young Generation
- 老年代Old Generation
- 永久代Permanent Generation
Hotspot 堆記憶體結構
下面我將介紹每個“代”及其主要特徵。
新生代
所有建立不久的物件都存放在這裡。新生代被進一步分為以下兩個區域:
- 伊甸區Eden:所有新建立的物件都在此處分配記憶體。
- 倖存者區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 處理程式中的變數和類例項的方式。這種理解使你能夠規劃和排除程式碼故障,並理解特定平臺固有的潛在限制。
- 61秒,摸透Linux的健康狀態!
- 首屆SHIFT數字化轉型峰會開幕,第四正規化釋出4Paradigm Shift企業轉型平臺及服務
- HarmonyOS應用開發:鴻蒙JS實戰,計算器功能開發!
- 人工智慧在時尚行業的應用
- 用Python自動生成 圖文並茂的資料分析 報告
- Vitest:替代 Jest 的前端測試工具新選擇
- 從滑動標尺模型看企業網路安全能力評估與建設
- 如何為智慧城市建立經濟高效的資料儲存基礎設施
- Vue的遞迴元件:渲染巢狀評論
- 在 Web 應用的執行時實現多分支並存和切換
- 雲端計算下半場:誰主沉浮?
- Umd 的包如何匯出 TS 型別
- Volatile:JVM 我警告你,我的人你別亂動
- GitLab禁止員工使用Windows、推特確認540萬賬戶資料洩露、淘寶宣佈上線方言語音搜功能 | T資訊
- 資料中心的停機時間怎麼會成為企業的噩夢?
- 如何保護智慧家居避免黑客攻擊
- 企業從雲服務的承諾支出中獲得最大收益的四種方法
- “整潔架構” 和商家前端的重構之路
- 中文文字智慧糾錯知多少?
- 程式設計師不得不知道的 API 介面常識