詳解JVM如何判斷一個對象是否可以被回收
在c++中,當(dāng)我們使用完某個對象的時候,需要顯示的將對象回收,如果忘記回收,則會導(dǎo)致無用對象一直在內(nèi)存里,導(dǎo)致內(nèi)存泄露。在java中,jvm會幫助我們進行垃圾回收,無需程序員自己寫代碼進行回收。
首先jvm需要解決的問題是:如何判斷一個對象是否是垃圾,是否可以被回收呢?一般都是通過引用計數(shù)法,可達性算法。
引用計數(shù)法
對每個對象的引用進行計數(shù),每當(dāng)有一個地方引用它時計數(shù)器+1、引用失效(改為引用其他對象,賦值為null,或者生命周期結(jié)束)則-1,引用的計數(shù)放到對象頭中,大于0的對象被認為是存活對象,一旦某個對象的引用計數(shù)器為 0,則說明該對象已經(jīng)死亡,便可以被回收了。
public void f(){ Object a = new Object(); // 對象a引用計數(shù)為1 g(a); // 退出g(a),對象b的生命周期結(jié)束,對象a引用計數(shù)為1 }// 退出f(), 對象a的生命周期結(jié)束,引用計數(shù)為0 public void g(Object a){ Object b = a; // 對象a引用計數(shù)為2 Object c = a; // 對象a引用計數(shù)為3 Object d = a; // 對象a引用計數(shù)為4 d = new Object(); // 對象a引用計數(shù)為3 c = null; // 對象a引用計數(shù)為2 }
引用計數(shù)法實現(xiàn)起來比較容易,但是存在一個嚴重的問題,那就是無法檢測循環(huán)依賴。如下所示:
public class A{ public B b; public A(){ } } public class A{ public A a; public B(){ } } A a = new A(); // a的計數(shù)為1 B b = new B(); // b的計數(shù)為1 a.b = b; // b的計數(shù)為2 b.a = a; // a的計數(shù)為2 a = null; // a的計數(shù)為1 b = null; // b的計數(shù)為1
最終a,b的計數(shù)都為1,無法被識別為垃圾,所以無法被回收。
Python使用的就是引用計數(shù)算法,Python的垃圾回收機制,很大一部分是為了處理可能產(chǎn)生的循環(huán)引用,是對引用計數(shù)的補充。
雖然循環(huán)引用的問題可通過Recycler算法解決,但是在多線程環(huán)境下,引用計數(shù)變更也要進行昂貴的同步操作,性能較低,早期的編程語言會采用此算法。
可達性算法
介紹
Java最終并沒有采用引用計數(shù)算法,JVM的主流垃圾回收器采取的是可達性分析算法。
我們把對象之間的引用關(guān)系用數(shù)據(jù)結(jié)構(gòu)中的有向圖來表示。圖中的頂點表示對象。如果對象A中的變量引用了對象B,那么,我們便在對象A對應(yīng)的頂點和對象B對應(yīng)的頂點之間畫一條有向邊。
在有向圖中,有一組特殊的頂點,叫做GC Roots。哪些對象可以作為GC Roots呢?
- 系統(tǒng)加載的類:rt.jar。
- JNI handles。
- 線程運行棧上所有引用,包括方法參數(shù),創(chuàng)建的局部變量等。
- 已啟動未停止的java線程。
- 已加載類的靜態(tài)變量。
- 用于同步的監(jiān)控,調(diào)用了對象的wait()/notify()/notifyAll()。
JVM以GC Roots為起點,遍歷(深度優(yōu)先遍歷或廣度優(yōu)先遍歷)整個圖,可以遍歷到的對象為可達對象,也叫做存活對象,遍歷不到的對象為不可達對象,也叫做死亡對象。死亡對象會被虛擬機當(dāng)做垃圾回收。
JVM實際上采用的是三色算法來遍歷整個圖的,遍歷走過的路徑被稱為reference chain。
- Black: 對象可達,且對象的所有引用都已經(jīng)掃描了(“掃描”在可以理解成遍歷過了或加入了待遍歷的隊列)
- Gray: 對象可達,但對象的引用還沒有掃描過(因此 Gray 對象可理解成在搜索隊列里的元素)
- White: 不可達對象或還沒有掃描過的對象
引用級別
遍歷到的對象一定會存活嗎?事實上,JVM會根據(jù)對象A對對象B的引用強不強烈作出相應(yīng)的回收措施。
基于此JVM根據(jù)引用關(guān)系的強烈,將引用關(guān)系分為四個等級:強引用,軟引用,弱引用,虛幻引用。
強引用
類似Object obj = new Object()
這類的引用都屬于強引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象,只有在和GC Roots斷絕關(guān)系時,才會被回收。
如果要對強引用進行垃圾回收,需要設(shè)置強引用對象為 null,或者讓其超出對象的生命周期范圍,則認為改對象不存在引用。類似obj = null;
參考代碼:
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
軟引用
用于描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常??梢允褂?code>SoftReference 類來實現(xiàn)軟引用。
Object obj = new Object(); SoftReference<Object> softRef = new SoftReference(obj);
弱引用
也是用于描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象??梢允褂?code>WeakReference 類來實現(xiàn)弱引用。
Object obj = new Object(); WeakReference<Object> weakReference = new WeakReference<>(obj); obj = null; System.gc(); TimeUnit.SECONDS.sleep(200); System.out.println(weakReference.get()); System.out.println(weakReference.isEnqueued());
虛引用
它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置一個虛引用關(guān)聯(lián)的唯一目的是能在這個對象被垃圾回收時收到一個系統(tǒng)通知。可以通過PhantomReference
來實現(xiàn)虛引用。
Object obj = new Object(); ReferenceQueue<Object> refQueue = new ReferenceQueue<>(); PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue); System.out.println(phantomReference.get()); System.out.println(phantomReference.isEnqueued());
基于虛引用,有一個更加優(yōu)雅的實現(xiàn)方式,那就是Java 9以后新加入的Cleaner,用來替代Object類的finalizer方法。
STW
雖然可達性分析的算法本身很簡明,但是在實踐中還是有不少其他問題需要解決的。我們把運行應(yīng)用程序的線程叫做用戶線程,把執(zhí)行垃圾回收的線程叫做垃圾回收線程,如果在執(zhí)行垃圾回收線程的同時還在執(zhí)行用戶線程,那么對象的引用關(guān)系可能會在垃圾回收途中被用戶線程修改,從而造成誤報(將引用設(shè)置為 null)或者漏報(將引用設(shè)置為未被訪問過的對象)
誤報并沒有什么傷害,Java 虛擬機至多損失了部分垃圾回收的機會。漏報則比較麻煩,因為垃圾回收器可能回收事實上仍被引用的對象內(nèi)存,導(dǎo)致程序出錯。
為了解決漏報的問題,保證垃圾回收線程不會被用戶線程打擾,最簡單粗暴的方式就是在垃圾回收的過程中,暫停用戶線程,直到垃圾回收結(jié)束,再恢復(fù)用戶線程,這就是STW(STOP THE WORLD)。
但是如果STW的時間過程,就會嚴重影響程序的性能,因此優(yōu)化垃圾回收過程,盡量減少STW的時間,是垃圾回收器努力優(yōu)化的方向,
安全點
上述除了STW的響應(yīng)時間的問題,還有另外一個問題,就是如何從一個正確的狀態(tài)停止,再從這個狀態(tài)正確恢復(fù)。Java虛擬機中的STW是通過安全點(safepoint)機制來實現(xiàn)的。當(dāng)Java虛擬機收到STW請求,它便會等待所有的線程都到達安全點,才允許請求Stop-the-world的線程進行獨占的工作。
當(dāng)然,安全點的初始目的并不是讓用戶線程立刻停下,而是找到一個穩(wěn)定的執(zhí)行狀態(tài)。在這個執(zhí)行狀態(tài)下,JVM的堆棧不會發(fā)生變化。這么一來,垃圾回收器便能夠“安全”地執(zhí)行可達性分析,才能找到完整GC Roots。
是不是所有的用戶線程在垃圾回收的時候都要停止呢?實際上,JVM也做了優(yōu)化,如果某個線程處于安全區(qū)(不會改變對象引用關(guān)系的一段連續(xù)的代碼區(qū)間),那么這個線程不需要停止,可以和垃圾回收線程并行執(zhí)行。一旦離開安全區(qū),JVM會檢查是否處于STW階段,如果是,則需要阻塞該線程,等垃圾回收完再恢復(fù)。
以上就是詳解JVM如何判斷一個對象是否可以被回收的詳細內(nèi)容,更多關(guān)于JVM對象回收的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一篇文章帶你入門Java數(shù)據(jù)結(jié)構(gòu)
這篇文章主要介紹了Java常見數(shù)據(jù)結(jié)構(gòu)面試題,帶有答案及解釋,希望對廣大的程序愛好者有所幫助,同時祝大家有一個好成績,需要的朋友可以參考下,希望可以幫助到你2021-08-08SpringBoot中TypeExcludeFilter的作用及使用方式
在SpringBoot應(yīng)用程序中,TypeExcludeFilter通過過濾特定類型的組件,使它們不被自動掃描和注冊為bean,這在排除不必要的組件或特定實現(xiàn)類時非常有用,通過創(chuàng)建自定義過濾器并注冊到spring.factories文件中,我們可以在應(yīng)用啟動時生效2025-01-01Data Source與數(shù)據(jù)庫連接池簡介(JDBC簡介)
DataSource是作為DriverManager的替代品而推出的,DataSource 對象是獲取連接的首選方法,這篇文章主要介紹了Data Source與數(shù)據(jù)庫連接池簡介(JDBC簡介),需要的朋友可以參考下2022-11-11Java如何利用return結(jié)束方法調(diào)用
這篇文章主要介紹了Java如何利用return結(jié)束方法調(diào)用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02IDEA的Terminal無法執(zhí)行g(shù)it命令問題
這篇文章主要介紹了IDEA的Terminal無法執(zhí)行g(shù)it命令問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09