當JAVA垃圾收集器(GC)運行時,它作為一個守護線程在后臺運行,用于為用戶線程提供服務或執行JVM任務。它定期檢查堆內存中的所有對象,并識別那些不再被程序的任何部分引用的對象(也就是不再被引用的對象)。然后,這些不再被引用的對象將被銷毀,并釋放空間供新創建的對象使用。
以下是Java垃圾收集過程的簡單步驟:
1.標記(Mark):從GC根開始,識別當前正在使用和未使用的對象引用,未使用的對象被標記為垃圾。2.清除(Sweep):遍歷堆,并找到存活對象之間的未使用空間,將這些空間記錄在一個空閑列表中,供將來的對象分配使用。3.壓縮(Compact):將所有存活的對象移動到一個連續的內存區域中,以提高新對象的內存分配性能。
然而,這種方法存在一些問題:
•效率低下,因為大多數新創建的對象很快就會變得無用。•長壽命對象很可能在將來的GC周期中仍然被使用。
為了解決這些問題,實際上,新創建的對象會根據其存活時間存儲在堆的不同代空間中。然后,在執行完整的垃圾收集之前,垃圾收集會在兩個主要階段進行,稱為Minor GC(年輕代垃圾收集)和Major GC(老年代垃圾收集),對象會在這些代之間進行掃描和移動。下圖展示了堆內存的這種劃分:
JVM堆內存
標記-清除模型
標記-清除模型是Java垃圾收集的基本實現。它有兩個主要階段:
1.標記(Mark):從GC根開始,識別并標記所有仍然被引用的對象,其余的被認為是垃圾。2.清除(Sweep):遍歷堆,并找到存活對象之間的未使用空間,將這些空間記錄在一個空閑列表中,供將來的對象分配使用。
Java垃圾收集的根
現在你知道了,當沒有引用指向一個對象時,它變得不可訪問,因此也成為垃圾收集的候選對象。等等,這是什么意思?引用是指什么?那么第一個引用是什么?我最初也有同樣的問題。讓我解釋一下這些引用和可達性在底層是如何發生的。
為了讓你的應用程序代碼能夠訪問一個對象,必須存在一個根對象,它與你的對象相連,并且能夠從堆外部訪問。這些從堆外部可以訪問的根對象稱為垃圾收集(GC)根。垃圾收集根有幾種類型,比如局部變量、活動的Java線程、靜態變量、JNI引用等(只是了解這里的思想,如果你進行快速的谷歌搜索,可能會找到許多關于GC根的不一致的分類)。我們需要學習的是,只要我們的對象被這些GC根之一直接或間接引用,并且GC根保持活動狀態,我們的對象就可以被認為是可達的。一旦我們的對象失去與GC根的引用,它就變得不可達,因此可以進行垃圾收集。
可達性與垃圾收集的資格
垃圾收集器只會銷毀不可達的對象。它是在后臺自動進行的過程,一般情況下,程序員不需要對此做任何操作。
注意:在銷毀對象之前,垃圾收集器最多會在該對象上調用一次
finalize()
方法(finalize()
方法不會對任何給定的對象多次調用)。默認的finalize()
方法為空實現。通過重寫它,我們可以執行一些清理活動,比如關閉數據庫連接或驗證對象的結束,就像我下面寫的那樣。一旦finalize()
方法完成,垃圾收集器將銷毀該對象。
考慮下面的Person
類,它有一個對象構造函數和finalize()
方法:
class Person {
// 存儲人員(對象)的名稱
String name;
public Person(String name) {
this.name = name;
}
@Override
/* 重寫finalize方法,以檢查哪個對象被垃圾收集 */
protected void finalize() throws Throwable {
// 將打印出人員(對象)的名稱
System.out.println("Person對象 - " + this.name + " -> 成功被垃圾收集");
}
}
如果滿足以下任一情況,對象可以立即變為不可達(無需等待堆中的分代老化)。
情況1:將引用變量置為null
當一個對象的引用變量被改為null
時,該對象變得不可達,從而可以進行垃圾收集。
// 創建一個Person對象
// new運算符為對象動態分配內存并返回對它的引用
Person p1 = new Person("John Doe");
// 在使用p1期間進行一些有意義的工作
...
...
...
// p1不再使用
// 使p1有資格進行垃圾回收
p1 = null;
// 調用垃圾收集器
System.gc(); // p1將被垃圾收集
輸出將是:
Person對象 - John Doe -> 成功被垃圾收集
情況2:重新分配引用變量
當一個對象的引用id被引用到另一個對象的引用id時,以前的對象將不再有引用指向它。該對象變得不可達,從而可以進行垃圾收集。// 創建兩個Person對象
// new運算符為對象動態分配內存并返回對它的引用
Person p1 = new Person("John Doe");
Person p2 = new Person("Jane Doe");
// 在使用p1和p2期間進行一些有意義的工作
...
...
...
// p1不再使用
// 使p1有資格進行垃圾回收
p1 = p2;
// 現在p1引用p2
// 調用垃圾收集器
System.gc(); // p1將被垃圾收集
輸出將是:
Person對象 - John Doe -> 成功被垃圾收集
情況3:在方法內創建對象
在上一篇文章中,我們了解了方法是如何按照LIFO(后進先出)順序存儲在堆棧中的。當這樣一個方法從堆棧中彈出時,它的所有成員都會消失,如果在其中創建了一些對象,那么這些對象也會變得不可達,因此可以進行垃圾收集。
class PersonTest {
static void createMale() {
// 在createMale()完成后,方法內的p1對象變得不可達
Person p1 = new Person("John Doe");
createFemale();
// 調用垃圾收集器
System.out.println("在createMale()中調用GC");
System.gc(); // p2將被
垃圾收集
}
static void createFemale() {
// 在createFemale()完成后,方法內的p2對象變得不可達
Person p2 = new Person("Jane Doe");
}
public static void mAIn(String args[]) {
createMale();
// 調用垃圾收集器
System.out.println("在main()中調用GC");
System.gc(); // p1將被垃圾收集
}
}
輸出將是:
在createMale()中調用GC
Person對象 - Jane Doe -> 成功被垃圾收集
在main()中調用GC
Person對象 - John Doe -> 成功被垃圾收集
情況4:匿名對象
當對象的引用ID未分配給變量時,該對象變得不可達,從而可以進行垃圾收集。
// 創建一個Person對象
// new運算符為對象動態分配內存并返回對它的引用
new Person("John Doe");
// 由于沒有變量分配,對象無法使用,因此它變得有資格進行垃圾回收
// 調用垃圾收集器
System.gc(); // 對象將被垃圾收集
輸出將是:
Person對象 - John Doe -> 成功被垃圾收集
情況5:只有內部引用的對象(孤島)
仔細觀察以下兩個對象如何失去其外部引用并成為垃圾收集的候選對象。
編程調用GC
盡管一個對象變得有資格進行垃圾收集,但它不會立即被垃圾收集器銷毀,因為JVM會按照一定的時間間隔運行GC。然而,使用以下任一方法,我們可以從JVM中編程地請求運行垃圾收集器(但仍然不能保證任何這些方法一定會運行垃圾收集器。GC完全由JVM決定)。
•使用System.gc()
方法•使用Runtime.getRuntime().gc()
方法
// 創建兩個Person對象
// new運算符為對象動態分配內存并返回對它的引用
Person p1 = new Person("John Doe");
Person p2 = new Person("Jane Doe");
// 在使用p1和p2期間進行一些有意義的工作
...
...
...
// p1和p2不再使用
// 使p1有資格進行垃圾回收
p1 = null;
// 調用垃圾收集器
System.gc(); // p1將被垃圾收集
// 使p2有資格進行垃圾回收
p2 = null;
// 調用垃
圾收集器
Runtime.getRuntime().gc(); // p2將被垃圾收集
輸出將是:
Person對象 - John Doe -> 成功被垃圾收集
Person對象 - Jane Doe -> 成功被垃圾收集