有沒有做q版頭像的網(wǎng)站seo在線培訓機構排名
圖解 JVM 垃圾回收(一)
- 1.前言
- 1.1 什么是垃圾
- 1.2 內存溢出和內存泄漏
- 2.垃圾回收的定義與重要性
- 3.GC 判斷策略
- 3.1 引用計數(shù)算法
- 3.2 可達性分析算法
- 4.引用類型
- 5.垃圾回收算法
- 5.1 標記-復制(Copying)
- 5.2 標記-清除(Mark-Sweep)
- 5.3 標記-整理(Mark-Compact)
- 5.4 分代收集理論
- 5.5 垃圾回收階段算法小結
1.前言
1.1 什么是垃圾
垃圾 是指運行程序中 沒有任何引用指向的對象,需要被回收。
1.2 內存溢出和內存泄漏
內存溢出:經(jīng)過垃圾回收之后,內存仍舊無法存儲新創(chuàng)建的對象,內存不夠溢出。
內存泄漏:又叫 “存儲泄漏”,對象不會再被程序使用了,但是 GC 又不能回收它們。例如:IO 流不適用了但是沒有被 Close、數(shù)據(jù)庫連接 JDBC 沒有被 Close。這些對象不會被回收就會占據(jù)內存,大量的此類對象存在,也是導致內存溢出的原因。
2.垃圾回收的定義與重要性
垃圾回收(Garbage Collection
,簡稱 GC
)是內存管理的核心組成部分,它負責自動回收不再使用的內存空間。在 Java 中,程序員不需要手動釋放對象占用的內存,一旦對象不再被引用,垃圾回收器就會在適當?shù)臅r機回收它們所占用的內存。這樣可以避免 內存泄漏 和 野指針,從而大大減輕了程序員的負擔,也使得 Java 成為一個相對安全、易于開發(fā)的編程語言。
- 防止內存泄漏:手動管理內存容易導致內存泄漏,而 GC 可以自動回收不再使用的對象,防止內存泄漏的發(fā)生。
- 提高開發(fā)效率:程序員不再需要關心內存釋放的問題,可以更加集中精力在業(yè)務邏輯的實現(xiàn)上。
- 系統(tǒng)性能和穩(wěn)定性:通過有效的垃圾回收策略,可以保證系統(tǒng)的性能和穩(wěn)定性。
垃圾回收的基本步驟分兩步:
- 1?? 查找內存中不再使用的對象(GC 判斷策略)
- 2?? 釋放這些對象占用的內存(GC 收集算法)
3.GC 判斷策略
3.1 引用計數(shù)算法
給對象添加一個引用計數(shù)器,當對象增加一個引用時計數(shù)器加 1,引用失效時計數(shù)器減 1。引用計數(shù)為 0 的對象可被回收。兩個對象出現(xiàn)循環(huán)引用的情況下,此時引用計數(shù)器永遠不為 0,導致無法對它們進行回收。正因為循環(huán)引用的存在,因此 Java 虛擬機不使用引用計數(shù)算法。
public class ReferenceCountingGC {public Object instance = null;public static void main(String[] args) {ReferenceCountingGC objectA = new ReferenceCountingGC();ReferenceCountingGC objectB = new ReferenceCountingGC();objectA.instance = objectB;objectB.instance = objectA;}
}
3.2 可達性分析算法
通過 GC Roots
作為起始點進行搜索,能夠到達到的對象都是存活的,不可達的對象可被回收。
哪些對象可以作為 GC Roots
呢?
- 虛擬機棧(棧幀中的局部變量表)中引用的對象
- 本地方法棧(Native 方法)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 所有被同步鎖持有的對象
JNI
(Java Native Interface
)引用的對象
public void method() { Object localVariable = new Object(); // localVariable 是 GC Roots
}public class MyClass { private static Object staticObject = new Object(); // staticObject 是 GC Roots
}public class MyClass { private static final String CONSTANT_STRING = "constant"; // CONSTANT_STRING 是 GC Roots
}public synchronized void synchronizedMethod() { // 當前對象(this)在執(zhí)行同步方法時是 GC Roots
}
4.引用類型
無論是通過引用計算算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象是否可達,判定對象是否可被回收都與引用有關。
Java 中有四種類型的引用,它們對垃圾回收的影響不同:
- 強引用(
Strong Reference
):最常見的引用類型,只要對象有強引用指向,它就不會被垃圾回收。 - 軟引用(
Soft Reference
):軟引用可以幫助垃圾回收器回收內存,只有在內存不足時,軟引用指向的對象才會被回收。 - 弱引用(
Weak Reference
):弱引用指向的對象在下一次垃圾回收時會被回收,不管內存是否足夠。 - 虛引用(
Phantom Reference
):虛引用的主要用途是跟蹤對象被垃圾回收的狀態(tài),虛引用指向的對象總是可以被垃圾回收。
import java.lang.ref.*;public class ReferenceTypes {public static void main(String[] args) {Object strongRef = new Object(); // 強引用SoftReference<Object> softRef = new SoftReference<>(new Object()); // 軟引用WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>()); // 虛引用System.gc(); // 觸發(fā)垃圾回收System.out.println("Strong Reference: " + strongRef);System.out.println("Soft Reference: " + softRef.get());System.out.println("Weak Reference: " + weakRef.get());System.out.println("Phantom Reference: " + phantomRef.get());}
}
5.垃圾回收算法
5.1 標記-復制(Copying)
它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區(qū)間的一半進行回收。
- ? 優(yōu)點: 減少內存碎片,提高空間利用率。
- ? 缺點: 減半了可用的堆內存,可能增加垃圾回收的頻率。
5.2 標記-清除(Mark-Sweep)
算法分為“標記”和“清除”階段:
標記清除算法分為兩個主要步驟:標記 和 清除。
- 標記階段: 在標記階段,垃圾回收器會從
GC Roots
開始,遍歷所有可達的對象,并標記它們?yōu)榛顒訉ο蟆?/li> - 清除階段: 在清除階段,垃圾回收器會遍歷整個堆,回收所有未被標記的對象的內存。
有兩個明顯的問題:
- 效率問題:如果需要標記的對象太多,效率不高。
- 空間問題:標記清除后會產生大量不連續(xù)的內存碎片,空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續(xù)內存而不得不提前觸發(fā)另一次垃圾收集。
5.3 標記-整理(Mark-Compact)
標記整理算法是標記清除算法的改進版本。它在標記和清除的基礎上增加了整理階段,將所有活動對象向一端移動,從而消除內存碎片。
- ? 優(yōu)點: 解決了內存碎片化問題,提高了空間利用率。
- ? 缺點: 移動對象增加了額外的開銷。
5.4 分代收集理論
當前虛擬機的垃圾收集都采用分代收集算法,根據(jù)對象存活周期的不同將內存分為幾塊。一般將 Java 堆分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法。
- 新生代(
Young Generation
):使用復制算法,因為新生代中的對象生命周期較短。 - 老年代(
Tenured Generation
):使用標記整理或標記清除算法,因為老年代中的對象生命周期較長,且數(shù)量較少。
1?? 新生代的回收算法(以 復制 算法為主)
- 所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
- 新生代內存按照 8 : 1 : 1 8:1:1 8:1:1 的比例分為一個
eden
區(qū)和兩個survivor
(survivor0
,survivor1
)區(qū)(一般而言)。大部分對象在eden
區(qū)中生成。回收時先將eden
區(qū)存活對象復制到一個survivor0
區(qū),然后清空eden
區(qū),當這個survivor0
區(qū)也存放滿了時,則將eden
區(qū)和survivor0
區(qū)存活對象復制到另一個survivor1
區(qū),然后清空eden
和這個survivor0
區(qū),此時survivor0
區(qū)是空的,然后將survivor0
區(qū)和survivor1
區(qū)交換,即保持survivor1
區(qū)為空, 如此往復。 - 當
survivor1
區(qū)不足以存放eden
和survivor0
的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發(fā)一次Full GC
(Major GC
),也就是新生代、老年代都進行回收。 - 新生代發(fā)生的 GC 也叫做
Minor GC
,Minor GC
發(fā)生頻率比較高(不一定等eden
區(qū)滿了才觸發(fā))。
2?? 老年代的回收算法(以 標記-清除、標記-整理 為主)
- 在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到老年代中。因此,可以認為老年代中存放的都是一些生命周期較長的對象。
- 內存比新生代也大很多(大概比例是 1 : 2 1:2 1:2),當老年代內存滿時觸發(fā)
Major GC
,Major GC
發(fā)生頻率比較低,老年代對象存活時間比較長,存活率標記高。
3?? 永久代(Permanet Generation
)的回收算法
JDK 1.8 及以后方法區(qū)的實現(xiàn)變成了元空間。
用于存放靜態(tài)文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態(tài)生成或者調用一些 class,例如 Hibernate 等,在這種時候需要設置一個比較大的永久代空間來存放這些運行過程中新增的類。永久代也稱 方法區(qū)。方法區(qū)主要回收的內容有:廢棄常量和無用的類。對于廢棄常量也可通過根搜索算法來判斷,但是對于無用的類則需要同時滿足下面 3 個條件:
- 該類所有的實例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實例。
- 加載該類的 ClassLoader 已經(jīng)被回收。
- 該類對應的
java.lang.Class
對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
5.5 垃圾回收階段算法小結
標記復制 | 標記清除 | 標記壓縮 | |
---|---|---|---|
速率 | 最快 | 中 | 最慢 |
空間開銷 | 兩個大小相同的空間 | 少(會堆積碎片) | 少(不會碎片堆積) |
移動對象 | 是 | 否 | 是 |