在方法中會建立大量的物件,物件並不一定是全域性都會使用的,並且Java虛擬機器器的資源是有限的
當JVM(Java虛擬機器器)判斷物件不再使用時,就會將其回收,避免佔用資源
那麼JVM是如何判斷物件不再使用的呢?
本篇文章將圍繞判斷物件是否再使用,深入淺出的解析引用計數法、可達性分析演演算法以及JVM如何判斷物件是真正的“死亡”(不再使用)
判斷物件已死
引用計數演演算法
引用計數演演算法判斷物件已死
在物件新增一個引用計數器,有地方引用此物件該引用計數器+1,引用失效時該引用計數器-1;當引用計數器為0時,說明沒有任何地方引用物件,物件可以被回收
但是該方法無法解決迴圈引用(比如物件A的欄位引用了物件B,物件B的欄位引用了欄位A,此時都將null賦值給物件A,B它們的引用計數器上都不為0,也就是表示物件還在被引用,但實際上已經沒有引用了)
- 優點 : 標記“垃圾”物件簡單,高效
- 缺點: 無法解決迴圈引用,儲存引用計數器的空間開銷,更新引用記數的時間開銷
因為無法解決迴圈引用所以JVM不使用引用計數法
引用計數方法常用在不存在迴圈引用的時候,比如Redis中使用引用計數,不存在迴圈引用
證明Java未採用引用計數演演算法
public class ReferenceCountTest {
//佔用記憶體
private static final byte[] MEMORY = new byte[1024 * 1024 * 2];
private ReferenceCountTest reference;
public static void main(String[] args) {
ReferenceCountTest a = new ReferenceCountTest();
ReferenceCountTest b = new ReferenceCountTest();
//迴圈引用
a.reference = b;
b.reference = a;
a = null;
b = null;
// System.gc();
}
}
可達性分析演演算法
Java使用可達性分析演演算法,可以解決迴圈引用
可達性分析演演算法判斷物件已死
從
GC Roots
物件開始,根據引用關係向下搜尋,搜尋的過程叫做引用鏈- 如果透過
GC Roots
可以透過引用鏈達到某個物件則該物件稱為引用可達物件 - 如果透過
GC Roots
到某個物件沒有任何引用鏈可以達到,就把此物件稱為引用不可達物件,將它放入引用不可達物件集合中(如果它是首個引用不可達物件節點,那它就是引用不可達物件根節點)
- 如果透過
可以作為GC Roots物件的物件
- 在棧幀中區域性變數表中引用的物件引數、臨時變數、區域性變數
- 本地方法引用的物件
- 方法區的類變數引用的物件
- 方法區的常量引用的物件(字串常量池中的引用)
- 被
sychronized
同步鎖持有的物件 - JVM內部引用(基礎資料型別對應的Class物件、系統類載入器、常駐異常物件等)
- 跨代引用
缺點:
- 使用可達性分析演演算法必須在保持一致性的快照中進行(某時刻靜止狀態)
- 這樣在進行GC時會導致STW(Stop the Word)從而讓使用者執行緒短暫停頓
真正的死亡
真正的死亡最少要經過2次標記
- 透過GC Roots經過可達性分析演演算法,得到某物件不可達時,進行第一次標記該物件
接著進行一次篩選(篩選條件: 此物件是否有必要執行
finalize()
)- 如果此物件沒有重寫
finalize()
或JVM已經執行過此物件的finalize()
都將被認為此物件沒有必要執行finalize()
,這個物件真正的死亡了 如果認為此物件有必要執行
finalize()
則會把該物件放入F-Queue
佇列中,JVM自動生成一條低優先順序的Finalizer執行緒- Finalizer執行緒是守護執行緒,不需要等到該執行緒執行完才結束程式,也就是說不一定會執行該物件的finalize()方法
- 設計成守護執行緒也是為了防止執行finalize()時會發生阻塞,導致程式時間很長,等待很久
- Finalize執行緒會掃描
F-Queue
佇列,如果此物件的finalize()
方法中讓此物件重新與引用鏈上任一物件搭上關係,那該物件就完成自救finalize()方法是物件自救的最後機會
- 如果此物件沒有重寫
測試不重寫finalize()方法,物件是否會自救
/**
* @author Tc.l
* @Date 2020/11/20
* @Description:
* 測試不重寫finalize方法是否會自救
*/
public class DeadTest01 {
public static DeadTest01 VALUE = null;
public static void isAlive(){
if(VALUE!=null){
System.out.println("Alive in now!");
}else{
System.out.println("Dead in now!");
}
}
public static void main(String[] args) {
VALUE = new DeadTest01();
VALUE=null;
System.gc();
try {
//等Finalizer執行緒執行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
isAlive();
}
}
/*
Dead in now!
*/
物件並沒有發生自救,物件不再使用“已死”
測試重寫finalize()方法,物件是否會自救
/**
* @author Tc.l
* @Date 2020/11/20
* @Description:
* 測試重寫finalize方法是否會自救
*/
public class DeadTest02 {
public static DeadTest02 VALUE = null;
public static void isAlive(){
if(VALUE!=null){
System.out.println("Alive in now!");
}else{
System.out.println("Dead in now!");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("搭上引用鏈的任一物件進行自救");
VALUE=this;
}
public static void main(String[] args) {
VALUE = new DeadTest02();
System.out.println("開始第一次自救");
VALUE=null;
System.gc();
try {
//等Finalizer執行緒執行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
isAlive();
System.out.println("開始第二次自救");
VALUE=null;
System.gc();
try {
//等Finalizer執行緒執行
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
isAlive();
}
}
/*
開始第一次自救
搭上引用鏈的任一物件進行自救
Alive in now!
開始第二次自救
Dead in now!
*/
第一次自救成功,第二次自救失敗,說明瞭finalize()執行過,JVM會認為它是沒必要執行的了
重寫finalize()代價高,不能確定各個物件執行順序,不推薦使用
總結
本篇文章圍繞如何判斷物件不再使用,深入淺出的解析引用計數法、可達性分析演演算法以及JVM中如何真正確定物件不再使用的
引用計數法使用計數器來記錄物件被引用的次數,當發生迴圈引用時無法判斷物件是否不再使用,因此JVM沒有使用引用計數法
可達性分析演演算法使用從根節點開始遍歷根節點的引用鏈,如果某個物件在引用鏈上說明這個物件被引用是可達的,不可達物件則額外記錄
可達性分析演演算法需要在保持一致性的快照中進行,在GC時會發生STW短暫的停頓使用者執行緒
可達性分析演演算法中的根節點一般是區域性變數表中引用的物件、方法中引用的物件、方法區靜態變數引用的物件、方法區常量引用的物件、鎖物件、JVM內部引用物件等等
當物件不可達時,會被放在佇列中由finalize守護執行緒來依次執行佇列中物件的finalize方法,如果第一次在finalize方法中搭上引用鏈則又會變成可達物件,注意finalize方法只會被執行一次,後續再不可達則會被直接認為物件不再使用
最後(一鍵三連求求拉~)
本篇文章筆記以及案例被收入 gitee-StudyJava、 github-StudyJava 感興趣的同學可以stat下持續關注喔\~
有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~
關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜
本文由部落格一文多發平臺 OpenWrite 釋出!