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