詳解JVM如何判斷一個對象是否可以被回收
在c++中,當(dāng)我們使用完某個對象的時候,需要顯示的將對象回收,如果忘記回收,則會導(dǎo)致無用對象一直在內(nèi)存里,導(dǎo)致內(nèi)存泄露。在java中,jvm會幫助我們進(jìn)行垃圾回收,無需程序員自己寫代碼進(jìn)行回收。
首先jvm需要解決的問題是:如何判斷一個對象是否是垃圾,是否可以被回收呢?一般都是通過引用計數(shù)法,可達(dá)性算法。
引用計數(shù)法
對每個對象的引用進(jìn)行計數(shù),每當(dāng)有一個地方引用它時計數(shù)器+1、引用失效(改為引用其他對象,賦值為null,或者生命周期結(jié)束)則-1,引用的計數(shù)放到對象頭中,大于0的對象被認(rèn)為是存活對象,一旦某個對象的引用計數(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)起來比較容易,但是存在一個嚴(yá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的垃圾回收機(jī)制,很大一部分是為了處理可能產(chǎn)生的循環(huán)引用,是對引用計數(shù)的補(bǔ)充。
雖然循環(huán)引用的問題可通過Recycler算法解決,但是在多線程環(huán)境下,引用計數(shù)變更也要進(jìn)行昂貴的同步操作,性能較低,早期的編程語言會采用此算法。
可達(dá)性算法
介紹
Java最終并沒有采用引用計數(shù)算法,JVM的主流垃圾回收器采取的是可達(dá)性分析算法。
我們把對象之間的引用關(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。
- 線程運(yùn)行棧上所有引用,包括方法參數(shù),創(chuàng)建的局部變量等。
- 已啟動未停止的java線程。
- 已加載類的靜態(tài)變量。
- 用于同步的監(jiān)控,調(diào)用了對象的wait()/notify()/notifyAll()。
JVM以GC Roots為起點,遍歷(深度優(yōu)先遍歷或廣度優(yōu)先遍歷)整個圖,可以遍歷到的對象為可達(dá)對象,也叫做存活對象,遍歷不到的對象為不可達(dá)對象,也叫做死亡對象。死亡對象會被虛擬機(jī)當(dāng)做垃圾回收。
JVM實際上采用的是三色算法來遍歷整個圖的,遍歷走過的路徑被稱為reference chain。
- Black: 對象可達(dá),且對象的所有引用都已經(jīng)掃描了(“掃描”在可以理解成遍歷過了或加入了待遍歷的隊列)
- Gray: 對象可達(dá),但對象的引用還沒有掃描過(因此 Gray 對象可理解成在搜索隊列里的元素)
- White: 不可達(dá)對象或還沒有掃描過的對象

引用級別
遍歷到的對象一定會存活嗎?事實上,JVM會根據(jù)對象A對對象B的引用強(qiáng)不強(qiáng)烈作出相應(yīng)的回收措施。
基于此JVM根據(jù)引用關(guān)系的強(qiáng)烈,將引用關(guān)系分為四個等級:強(qiáng)引用,軟引用,弱引用,虛幻引用。
強(qiáng)引用
類似Object obj = new Object() 這類的引用都屬于強(qiáng)引用,只要強(qiáng)引用還存在,垃圾回收器永遠(yuǎn)不會回收掉被引用的對象,只有在和GC Roots斷絕關(guān)系時,才會被回收。
如果要對強(qiáng)引用進(jìn)行垃圾回收,需要設(shè)置強(qiáng)引用對象為 null,或者讓其超出對象的生命周期范圍,則認(rèn)為改對象不存在引用。類似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)存溢出之前,將會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常??梢允褂?code>SoftReference 類來實現(xiàn)軟引用。
Object obj = new Object(); SoftReference<Object> softRef = new SoftReference(obj);
弱引用
也是用于描述非必需對象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(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
雖然可達(dá)性分析的算法本身很簡明,但是在實踐中還是有不少其他問題需要解決的。我們把運(yùn)行應(yīng)用程序的線程叫做用戶線程,把執(zhí)行垃圾回收的線程叫做垃圾回收線程,如果在執(zhí)行垃圾回收線程的同時還在執(zhí)行用戶線程,那么對象的引用關(guān)系可能會在垃圾回收途中被用戶線程修改,從而造成誤報(將引用設(shè)置為 null)或者漏報(將引用設(shè)置為未被訪問過的對象)
誤報并沒有什么傷害,Java 虛擬機(jī)至多損失了部分垃圾回收的機(jī)會。漏報則比較麻煩,因為垃圾回收器可能回收事實上仍被引用的對象內(nèi)存,導(dǎo)致程序出錯。
為了解決漏報的問題,保證垃圾回收線程不會被用戶線程打擾,最簡單粗暴的方式就是在垃圾回收的過程中,暫停用戶線程,直到垃圾回收結(jié)束,再恢復(fù)用戶線程,這就是STW(STOP THE WORLD)。
但是如果STW的時間過程,就會嚴(yán)重影響程序的性能,因此優(yōu)化垃圾回收過程,盡量減少STW的時間,是垃圾回收器努力優(yōu)化的方向,
安全點
上述除了STW的響應(yīng)時間的問題,還有另外一個問題,就是如何從一個正確的狀態(tài)停止,再從這個狀態(tài)正確恢復(fù)。Java虛擬機(jī)中的STW是通過安全點(safepoint)機(jī)制來實現(xiàn)的。當(dāng)Java虛擬機(jī)收到STW請求,它便會等待所有的線程都到達(dá)安全點,才允許請求Stop-the-world的線程進(jìn)行獨(dú)占的工作。
當(dāng)然,安全點的初始目的并不是讓用戶線程立刻停下,而是找到一個穩(wěn)定的執(zhí)行狀態(tài)。在這個執(zhí)行狀態(tài)下,JVM的堆棧不會發(fā)生變化。這么一來,垃圾回收器便能夠“安全”地執(zhí)行可達(dá)性分析,才能找到完整GC Roots。
是不是所有的用戶線程在垃圾回收的時候都要停止呢?實際上,JVM也做了優(yōu)化,如果某個線程處于安全區(qū)(不會改變對象引用關(guān)系的一段連續(xù)的代碼區(qū)間),那么這個線程不需要停止,可以和垃圾回收線程并行執(zhí)行。一旦離開安全區(qū),JVM會檢查是否處于STW階段,如果是,則需要阻塞該線程,等垃圾回收完再恢復(fù)。
以上就是詳解JVM如何判斷一個對象是否可以被回收的詳細(xì)內(nèi)容,更多關(guān)于JVM對象回收的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決java編譯錯誤( 程序包javax.servlet不存在javax.servlet.*)
這篇文章主要介紹了解決java編譯錯誤的相關(guān)資料,主要解決 程序包javax.servlet不存在javax.servlet.*的問題,需要的朋友可以參考下2017-08-08
IDEA創(chuàng)建Java項目導(dǎo)出Jar包運(yùn)行
這篇文章主要介紹了IDEA創(chuàng)建Java項目導(dǎo)出Jar包運(yùn)行,需要的朋友可以參考下2021-01-01
Spring+SpringMVC+MyBatis深入學(xué)習(xí)及搭建(一)之MyBatis的基礎(chǔ)知識
這篇文章主要介紹了Spring+SpringMVC+MyBatis深入學(xué)習(xí)及搭建(一)之MyBatis的基礎(chǔ)知識,需要的朋友可以參考下2017-05-05
Java實戰(zhàn)之用hutool-db實現(xiàn)多數(shù)據(jù)源配置
在微服務(wù)搭建中經(jīng)常會使用到多數(shù)據(jù)庫情形這個時候,下面這篇文章主要給大家介紹了關(guān)于Java實戰(zhàn)之用hutool-db實現(xiàn)多數(shù)據(jù)源配置的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12

