【Java】剛剛!突然!緊急通知!垃圾回收!
文章目錄
- 【Java】剛剛!突然!緊急通知!垃圾回收!
- 從C語言的內存管理引入:手動回收
- Java的垃圾回收機制
- 引用計數器
- 循環引用問題
- 可達性分析法與GC Root
- GC Root的典型例子
- 標記-清除算法
- 優點
- 缺點
- 復制算法
- 工作原理
- 優點
- 缺點
- 標記-整理算法
- 工作原理
- 優點
- 缺點
- 應用場景
- 分代回收機制
- 堆內存的分代
- 各代的垃圾回收策略
- 新生代垃圾回收(Minor GC)
- 老年代垃圾回收(Major GC 或 Full GC)
- 結語
本文將先簡要介紹C語言的手動內存回收機制,然后深入探討Java的垃圾回收(GC)機制,包括引用計數器、可達性分析法、GC root、標記-清除算法、復制算法、標記-整理算法以及分代回收機制。
從C語言的內存管理引入:手動回收
在C語言中,程序員需要手動管理內存的分配和釋放。這主要通過malloc
、calloc
、realloc
和free
函數來實現:
malloc
:用于動態分配內存。calloc
:類似于malloc
,但它會初始化分配的內存塊為零。realloc
:用于調整先前分配的內存塊的大小。free
:用于釋放動態分配的內存。
雖然這種手動管理提供了很大的靈活性,但也容易導致內存泄漏(未釋放不再使用的內存)和懸掛指針(指向已釋放內存的指針)。這就需要程序員特別小心,確保每一塊動態分配的內存都能被適時釋放。
例如:
我們使用malloc,為結點結構體的指針分配內存。
而在刪除節點時,我們采用free函數來進行內存的釋放。
Java的垃圾回收機制
與C語言不同,Java提供了自動內存管理功能。Java的垃圾回收機制旨在自動回收不再使用的對象所占用的內存,從而減輕程序員的負擔并提高內存管理的安全性。
引用計數器
最簡單的垃圾回收機制之一是引用計數器。它通過維護每個對象的引用計數來跟蹤對象是否可以被回收:
- 引用計數增加:每當一個新的引用指向對象時,引用計數加1。
- 引用計數減少:每當一個引用被銷毀或被設置為指向另一個對象時,引用計數減1。
- 回收對象:當對象的引用計數變為0時,說明該對象不再被使用,可以被回收。
然而,引用計數器存在一個明顯的缺點,即無法處理循環引用(兩個對象相互引用)。
循環引用問題
引用計數器的一大缺點是無法處理循環引用。如下示例:
class Node {Node next;
}Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
在這個例子中,即使a
和b
對象的引用離開了作用域,它們的引用計數器仍然不為0(因為它們互相引用),導致內存泄漏。
因此,Java并不使用這種方法作為其主要的垃圾回收策略。
可達性分析法與GC Root
Java采用的是更為先進的可達性分析法。它通過一組被稱為GC Root的根對象作為起點,沿著這些根對象的引用鏈進行搜索。如果一個對象能從GC Root到達,那么它就是可達的(alive),否則就是不可達的,可以被回收。
- GC Root:通常包括當前在棧中引用的對象、靜態變量引用的對象以及JNI(Java Native Interface)引用的對象等。
- 可達對象:從GC Root開始,所有可以通過引用鏈訪問到的對象都是可達的。
- 不可達對象:如果一個對象沒有從GC Root出發的任何引用鏈到達,則認為該對象是不可達的,可以被回收。
GC Root的典型例子
- 虛擬機棧中的引用對象:如棧幀中的局部變量和輸入參數。
- 方法區中的靜態引用:如類的靜態屬性。
- 方法區中的常量引用:如常量池中的引用。
標記-清除算法
標記-清除算法是最早且最基本的垃圾回收算法之一。標記-清除算法分為兩個階段:標記階段和清除階段。
- 標記階段:
- 從GC Root集合開始,遍歷對象引用圖,標記所有可達的對象。
- 標記過程通常是遞歸的,沿著對象引用鏈進行,直到所有可達的對象都被標記。
- 清除階段:
- 遍歷堆中的所有對象,回收未被標記的對象。
- 未標記的對象被認為是不可達的,可以被垃圾回收器回收。
優點
- 簡單直接:算法簡單,易于實現。
- 無需移動對象:對象在內存中的位置不會改變,減少了對象移動的開銷。
缺點
- 內存碎片:清除階段后,未被回收的對象會在堆中留下許多空閑區域,導致內存碎片。頻繁的內存碎片會降低內存分配效率。
- 標記和清除過程需要遍歷所有對象:在大堆內存中,遍歷所有對象可能導致較長的暫停時間。
復制算法
工作原理
復制算法將堆內存分為兩部分,通常是等大小的兩個半區:From空間和To空間。垃圾回收時,僅使用其中一個半區,另一個半區作為備用空間。
-
分配階段:
- 對象只在From空間中分配內存。
- To空間為空閑的,等待垃圾回收。
-
復制和清理階段:
- 從GC Root開始,遍歷所有可達的對象,并將它們復制到To空間。
- 復制過程中,保持對象的引用關系。
- 完成復制后,From空間中的所有對象被認為是不可達的,可以被回收。
- 交換From和To空間的角色,下一次分配和垃圾回收使用新的From空間。
-
示例:
假設有A塊等待垃圾回收:
回收之后會變成~:
優點
- 無內存碎片:對象被緊湊地復制到新的空間,不會留下內存碎片。
- 分配速度快:由于始終從一個連續的空閑區域分配內存,分配速度很快。
缺點
- 內存利用率低:由于堆內存被劃分為兩個半區,同時只使用一半內存,導致內存利用率低。
- 對象復制開銷:復制對象到新的空間需要額外的開銷,特別是當對象較多時。
標記-整理算法
標記-整理算法(Mark-Compact Algorithm)是一種改進的垃圾回收算法,用于解決標記-清除算法產生的內存碎片問題。它結合了標記-清除和復制算法的優點,通過整理內存來提高內存分配效率。下面將詳細分析標記-整理算法的工作原理、優缺點及其適用場景。
工作原理
標記-整理算法也分為兩個主要階段:標記階段和整理階段。
-
標記階段:
- 從GC Root集合開始,遍歷對象引用圖,標記所有可達的對象。
- 這一步與標記-清除算法中的標記階段相同,標記過程是遞歸的,沿著對象引用鏈進行,直到所有可達的對象都被標記。
-
整理階段:
- 遍歷整個堆,將所有存活的對象向一端移動(通常是堆的起始位置),保持對象之間的緊密排列。
- 更新所有對象的引用,以反映它們的新位置。
- 移動完成后,釋放未被標記對象的內存,未被標記的對象被回收,形成一塊連續的空閑區域。
優點
- 無內存碎片:對象被緊密排列在一起,沒有內存碎片,提高了內存利用率。
- 高效的內存分配:由于所有存活對象被移動到堆的一端,剩下的內存是連續的,內存分配速度更快。
- 適用于長生命周期對象:尤其適合老年代(Old Generation)的垃圾回收,因為老年代對象生命周期較長,不需要頻繁移動。
缺點
- 對象移動開銷:整理階段需要移動對象,并更新引用,增加了額外的開銷,尤其是在老年代中存活對象較多時。
- 暫停時間長:標記和整理過程會導致應用暫停,可能影響實時性要求較高的應用。
標記-整理算法減少了內存碎片化,同時避免了復制算法中需要雙倍內存的缺點。
應用場景
- 標記-清除算法:適用于內存緊張、不希望頻繁移動對象的場景,如老年代(Old Generation)的垃圾回收。
- 復制算法:適用于對象生命周期短、需要快速回收的場景,如新生代(Young Generation)的垃圾回收。JVM中的新生代垃圾回收器通常使用復制算法。
- 標記-整理算法:對象較大且數量較多的場景,當對象較大且數量較多時,標記-整理算法可以通過緊湊排列對象,減少內存浪費,提高內存利用率。
分代回收機制
分代回收機制的核心思想是將堆內存劃分為幾個代,根據對象的生命周期長短來進行不同的管理和回收。大多數對象的生命周期很短,少數對象存活時間較長。通過這種劃分,可以有針對性地采用不同的垃圾回收算法,提高回收效率和性能。
堆內存的分代
JVM中的堆內存通常劃分為以下幾代:
新生代(Young Generation):
- Eden區:所有新創建的對象首先分配在Eden區。
- 兩個Survivor區(S0和S1):用于在新生代中進行對象復制和存活對象的管理。每次垃圾回收時,存活的對象從Eden區和一個Survivor區復制到另一個Survivor區。
老年代(Old Generation):存活時間較長、從新生代晉升的對象存放在老年代。
永久代(PermGen)或元空間(Metaspace):存儲類的元數據(方法、類結構等)。在Java 8之前為永久代(PermGen),Java 8及之后為元空間(Metaspace)。
各代的垃圾回收策略
新生代垃圾回收(Minor GC)
新生代的垃圾回收頻繁,通常采用復制算法(Copying Algorithm)。當Eden區填滿時,觸發Minor GC:
- 存活對象復制:存活的對象從Eden區和一個Survivor區復制到另一個Survivor區。
- 對象晉升:當對象經過多次Minor GC后依然存活(達到一定年齡),或Survivor區空間不足時,存活對象晉升到老年代。
- Eden區清空:所有存活對象復制后,Eden區和一個Survivor區被清空,另一個Survivor區保留存活對象。
Minor GC的頻率較高,但由于新生代對象生命周期短,存活對象少,因此回收速度較快。
老年代垃圾回收(Major GC 或 Full GC)
老年代的垃圾回收通常采用標記-整理算法(Mark-Compact Algorithm) 或 標記-清除算法(Mark-Sweep Algorithm):
- 標記階段:從GC Root開始,標記所有可達的對象。
- 整理階段(標記-整理算法):將所有存活對象向一端移動,保持對象之間的緊密排列,釋放未標記對象的內存。
- 清除階段(標記-清除算法):回收未標記對象的內存,但可能產生內存碎片。
Major GC或Full GC的頻率較低,但由于老年代對象較多且存活時間長,回收過程較慢,可能導致較長的暫停時間。
結語
Java的垃圾回收機制通過自動化內存管理,極大地減輕了開發者的負擔,同時提升了程序的安全性和穩定性。盡管Java的GC機制復雜多樣,但其核心思想都是為了更高效地管理內存,避免內存泄漏和碎片化。
許多現代編程語言,如Java、C#、Python等,都內置了垃圾回收機制。通過學習垃圾回收,可以更深入地理解這些語言的設計思想和實現細節。不同語言的垃圾回收機制有所不同,了解這些差異可以幫助我們在具體語言中應用最佳實踐,編寫高效的代碼。
此后,筆者還會介紹Java中的垃圾回收器相關知識,以及有可能的JVM調優。
參考資料與文獻:
20、垃圾回收算法之可達性分析法和GC Roots是什么?_嗶哩嗶哩_bilibili
【JVM】萬字長文!深入詳解Java垃圾回收(GC)機制_java gc-CSDN博客