虛拟機如何确定一個對象是不是垃圾
引用計數算法
使用一個引用計數器記錄該對象還有多少個引用指針指向該對象,此算法簡單高效但需要在代碼中進行額外的邏輯處理以防止循環引用導緻記憶體洩露的問題。
讓我們來一起看看下面的例子來了解循環引用和記憶體洩漏兩個概念:
/**
* @author Zeng
* @date 2020/4/6 11:41
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 占點記憶體,以便觀察清楚GC日志中是否有進行垃圾收集
*/
private byte[] bigSize = new byte[2 * _1MB];
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
}
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
//對象内部的instance引用指針互相指向對方執行個體
objA.instance = objB;
objB.instance = objA;
//除了instance指針以外沒有其它引用指針指向這兩個對象
objA = null;
objB = null;
System.gc();
//執行finalize()方法的速度有些慢,讓主線程等待一下它執行
Thread.sleep(500);
}
public static void main(String[] args) {
testGC();
}
}
在引用計數算法中對于
objA
和
objB
是無法回收的,因為它們内部含有對方執行個體的引用指針,但是除此之外沒有其它指針引用這兩個對象,也無法通路到這兩個對象,JVM無法回收這兩個對象,這就導緻了記憶體洩漏。
執行結果如下:
可以看到JVM裡面并不是采用引用計數算法,因為在顯式指定垃圾收集時JVM确實把這兩個對象給回收了,這兩個對象的 finalize()
方法被調用了,這個方法是當對象第一次被回收時被調用的。
那麼JVM是如何确定對象是不是一個“垃圾”呢?
對象可達性分析
通過一系列的“GC Roots”根對象作為起始節點,一直往下搜尋引用關系,搜尋過程所走過的路徑稱為“引用鍊”。
看下面的代碼可以構成一條引用關系鍊,而
objD
因為沒有指針引用它而成為了
垃圾
,等待下一次垃圾回收來了結它。
/**
* @author Zeng
* @date 2020/4/7 7:44
*/
public class ReferenceList {
private String name;
public Object instance = null;
public ReferenceList(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(name + " finalize method executed!");
}
public static void main(String[] args) throws InterruptedException {
ReferenceList objA = new ReferenceList("objA");
ReferenceList objB = new ReferenceList("objB");
ReferenceList objC = new ReferenceList("objC");
objA.instance = objB;
objB.instance = objC;
objB = null;
objC = null;
ReferenceList objD = new ReferenceList("objD");
objD = null;
System.gc();
Thread.sleep(500);
}
}
上面代碼得到的引用關系鍊如下圖所示:
是以objD會被JVM回收,而objA、objB、objC可以存活下來,如下圖所示可以驗證這個結果:
固定作為GC Roots的對象主要有以下幾種:
虛拟機棧中引用的對象,例如局部變量、形式參數、臨時變量······
類靜态屬性引用的變量,例如類的靜态引用類型成員變量
常量引用的對象,例如
String str = "alive"
,字元串常量池中的引用。
同步鎖引用的對象,例如
中被鎖住的
synchronized(obj)
可作為
obj
GC Roots
細分的四種引用類型指定對象被回收的時機
強引用(Strongly Reference):即類似于
Object objA = new Object()
這種引用關系,垃圾收集器是永遠不會回收掉強引用的對象的。
軟引用(Soft Reference):用于表示還有用,但非必須的對象,隻有在發生記憶體溢出之前,才會回收這類對象。
弱引用(Weak Reference):被弱引用關聯的對象隻能存活到下一次垃圾回收發生為止,無論記憶體是否可用,都會回收該類對象。
虛引用(Phantom Reference):最差勁的一種引用關系,無法通過虛引用擷取對象執行個體,設定虛引用的目的就是為了讓它成為垃圾被回收掉。
下面例子可以證明它們引用回收的時機
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
/**
* @author Zeng
* @date 2020/4/7 8:16
* 四種引用類型實踐
*/
public class Reference {
private String name;
public Reference(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(name + " finalize method executed!");
}
public static void main(String[] args) throws InterruptedException {
//強引用
Reference obj = new Reference("強引用obj");
System.gc();
Thread.sleep(500);
//軟引用
Reference objA = new Reference("軟引用objA");
SoftReference<Reference> softReferenceA = new SoftReference<>(objA);
objA = null;
System.gc();
Thread.sleep(500);
//弱引用
Reference objB = new Reference("弱引用objB");
WeakReference<Reference> weakReferenceB = new WeakReference<>(objB);
objB = null;
System.gc();
Thread.sleep(500);
//虛引用
Reference objC = new Reference("虛引用objC");
ReferenceQueue<Reference> phantomQueue = new ReferenceQueue<>();
PhantomReference<Reference> phantomReference = new PhantomReference<>(objC, phantomQueue);
objC = null;
System.gc();
Thread.sleep(500);
}
}
運作結果:
可以看到弱引用和虛引用在下一輪垃圾回收時都會被當成垃圾給回收掉,而強引用和軟引用沒有被回收,如果構造出堆溢出的情況,軟引用也會被回收。
總結
本文主要講解了JVM如何确定一個對象是否可以回收,以及介紹引用計數法和可達性分析兩種方法判斷對象是否可回收,最後通過Java的四種引用類型的實踐驗證了強引用、軟引用、弱引用和虛引用的垃圾回收時機,如果有任何錯誤歡迎提出,樂意與大家交流學習!