java中如何判斷對(duì)象是否是垃圾
Java會(huì)自動(dòng)進(jìn)行內(nèi)存管理,JVM會(huì)進(jìn)行垃圾回收,哪它是怎么判定哪些是“垃圾”并決定“垃圾”的生死呢?
判斷對(duì)象是否為“垃圾”
Java有兩種算法判斷對(duì)象是否是垃圾:引用計(jì)數(shù)算法和可達(dá)性分析算法。
引用計(jì)數(shù)算法
引用計(jì)數(shù)(Reference Counting)算法就是給對(duì)象加一個(gè)引用計(jì)數(shù)器,當(dāng)對(duì)象被引用,計(jì)數(shù)器加一;當(dāng)引用失效時(shí),計(jì)數(shù)器減一;當(dāng)對(duì)象的引用計(jì)數(shù)器為0,對(duì)象就會(huì)被視為垃圾。
優(yōu)點(diǎn):
簡(jiǎn)單、判定垃圾效率高。
缺點(diǎn):
- 需要額外的空間存儲(chǔ)引用計(jì)數(shù)器
- 每當(dāng)一個(gè)引用被賦值給另一個(gè)引用時(shí),引用計(jì)數(shù)器就要進(jìn)行調(diào)整,增加了賦值語(yǔ)句時(shí)間
- 會(huì)出現(xiàn)循環(huán)引用。比如,對(duì)象a引用了對(duì)象b,同時(shí)對(duì)象b也引用了對(duì)象a,這就導(dǎo)致兩個(gè)對(duì)象之間循環(huán)引用。對(duì)象a和對(duì)象b的引用都不為0,即使這兩個(gè)對(duì)象已經(jīng)沒(méi)有其他引用,由于它們的引用計(jì)數(shù)都大于0,所以它們就沒(méi)有辦法被回收。如果要解決這個(gè)問(wèn)題就要引入額外機(jī)制,這樣效率又進(jìn)一步降低了。

引用計(jì)數(shù)算法在當(dāng)前主流的JVM中已經(jīng)沒(méi)有再被使用了。
簡(jiǎn)單的例子測(cè)試一下
public class ReferenceCountingTest {
public Object instance = null;
// 10M 占用內(nèi)存,便于分析
private byte[] bytes = new byte[10*1024*1024];
public static void main(String[] args) {
ReferenceCountingTest objectA = new ReferenceCountingTest();
ReferenceCountingTest objectB = new ReferenceCountingTest();
//互相引用
objectA.instance = objectB;
objectB.instance = objectA;
//切斷可達(dá)
objectA = null;
objectB = null;
//強(qiáng)制進(jìn)行垃圾回收
System.gc();
}
}
ReferenceCountingTest類(lèi)中有一個(gè)10M的byte數(shù)組, 讓objectA.instance = objectB和objectB.instance = objectA導(dǎo)致objectA和objectB互相引用,如果采用引用計(jì)數(shù)法的話,這兩個(gè)對(duì)象是沒(méi)法辦法進(jìn)行回收的,并且每個(gè)對(duì)象占用不少于10M的內(nèi)存空間。
在VM options 設(shè)置參數(shù) -XX:+PrintGC打印GC情況,來(lái)看下運(yùn)行結(jié)果是怎樣的:
[GC (System.gc()) 24381K->1106K(249344K), 0.0009894 secs] [Full GC (System.gc()) 1106K->957K(249344K), 0.0054511 secs]
從結(jié)果24381K->1106K可以看到內(nèi)存從24381K回收到1106K,回收的空間差不多就是objectA和objectB兩個(gè)對(duì)象占用的空間。這也從側(cè)面說(shuō)明JVM不是采用引用計(jì)數(shù)算法判定對(duì)象是否存活的。
可達(dá)性分析算法
可達(dá)性分析算法思路是使用一系列根對(duì)象(GC Roots)作為起點(diǎn),從根節(jié)點(diǎn)開(kāi)始向下進(jìn)行搜索,搜索過(guò)的路徑稱(chēng)為引用鏈(Reference Chain),如果某個(gè)對(duì)象到根節(jié)點(diǎn)沒(méi)有任何引用鏈相連或者說(shuō)從根節(jié)點(diǎn)到這個(gè)對(duì)象不可達(dá),則這個(gè)對(duì)象就被視為“垃圾”。

如上圖所示,白色橢圓形Object4、Object5、Object6之間雖然有關(guān)聯(lián),但是由于沒(méi)有和GC Roots關(guān)聯(lián),所以它們被判定為可回收對(duì)象。
在Java中有以下7種GC Roots:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。比如:方法入?yún)?、局部變量?/li>
- 方法區(qū)中常量引用的對(duì)象
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象:Java類(lèi)的引用類(lèi)型靜態(tài)變量
- 通過(guò)JNI調(diào)用本地代碼(nactive code)產(chǎn)生的JNI引用。包括JNI的局部變量或者全局變量
- 被系統(tǒng)類(lèi)加載器加載的類(lèi),這些類(lèi)不會(huì)被回收。它們可以以靜態(tài)字段的方式去持有對(duì)象。
- 所有被同步鎖(synchronized 關(guān)鍵)持有的對(duì)象
- 被JVM保留用于特殊目的的對(duì)象。哪些對(duì)象被保留取決于虛擬機(jī)的實(shí)現(xiàn),可能的有:系統(tǒng)類(lèi)加載器、一些重要的異常類(lèi)、做為異常類(lèi)處理的被預(yù)分配對(duì)象或者一些自定義的類(lèi)加載器。
以上8種GC Roots中前4個(gè)比較重要,在面試中也會(huì)經(jīng)常被問(wèn)到,后3個(gè)了解一下即可。
可達(dá)性分析算法是目前在動(dòng)態(tài)語(yǔ)言中使用最廣泛的算法,目前JVM判斷對(duì)象是否是垃圾用的都是這種算法。
垃圾的回收
Finalize方法
對(duì)象通過(guò)可達(dá)性分析算法被判定為可回收對(duì)象,也不是說(shuō)對(duì)象一定要被回收,對(duì)象可以通過(guò)重寫(xiě)finalize()方法獲得一次“免死”機(jī)會(huì)。當(dāng)發(fā)生GC的時(shí)候,JVM會(huì)判斷可回收的對(duì)象是否調(diào)用過(guò)finalize()方法,如果調(diào)用過(guò)finalize()方法,對(duì)象將會(huì)被回收;反之,如果沒(méi)有調(diào)用過(guò) finalize()方法,會(huì)將要調(diào)用finalize()方法的對(duì)象 F-Queue的隊(duì)列之中等待調(diào)用,在調(diào)用時(shí)如果對(duì)象重寫(xiě)了finalize()方法,可以在finalize()方法中“托關(guān)系想辦法”讓自己和GC Roots搭上關(guān)系進(jìn)行一次自我拯救,比如把自己(this關(guān)鍵字) 賦值給某個(gè)類(lèi)變量或者對(duì)象的成員變量,對(duì)象就會(huì)從即將回收的列表中移除,這樣對(duì)象就完成了一次自我拯救。在執(zhí)行完finalize()方法后,還會(huì)再判斷一次對(duì)象是否可達(dá),如果不可達(dá),自我拯救失敗,最后還是要被回收的。
要注意的一點(diǎn)是:對(duì)象finalize()方法只會(huì)調(diào)用一次,如果對(duì)象自我拯救成功一次,當(dāng)?shù)诙卧侔l(fā)生GC的時(shí)候會(huì)忽略調(diào)用對(duì)象的finalize()方法,最后都要被回收。這就是JVM世界的法則,只給對(duì)象一次不成為垃圾的機(jī)會(huì),如果再次成為垃圾,不好意思那只能被回收了。所以機(jī)會(huì)只有一次,要好好抓住。
下面通過(guò)例子測(cè)試一下對(duì)象的自我拯救:
public class FinalizeGC {
private static Object instance;
public static void main(String[] args) throws InterruptedException {
instance = new FinalizeGC();
instance = null;
//進(jìn)行第一次垃圾回收
System.gc();
//休眠1s
Thread.sleep(1000);
if (instance != null) {
System.out.println("I'm still alive.");
}else {
System.out.println("I'm dead.");
}
instance = null;
//進(jìn)行第二次垃圾回收
System.gc();
//休眠1s
Thread.sleep(1000);
if (instance != null) {
System.out.println("I'm still alive.");
}else {
System.out.println("I'm dead.");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Override finalize method execute");
instance = this;
}
}
運(yùn)行結(jié)果:
Override finalize method execute I'm still alive. I'm dead.
從運(yùn)行結(jié)果可以看到對(duì)象只被自我拯救一次,第二次自我拯救失敗。
讓線程休眠Thread.sleep(1000)1s是因?yàn)?code>F-Queue的隊(duì)列中的finalize()方法,會(huì)由一條由虛擬機(jī)自動(dòng)建立的、低調(diào)度優(yōu)先級(jí)的Finalizer線程去執(zhí)行它們,休眠是為了等待Finalizer線程去執(zhí)行finalize()方法。
把Thread.sleep(1000)注釋掉連續(xù)執(zhí)行多次,你可能會(huì)看到如下情況:
Override finalize method execute I'm dead. I'm dead.
或者
I'm dead. Override finalize method execute I'm dead.
出現(xiàn)上面的原因是finalize()方法執(zhí)行緩慢,對(duì)象還沒(méi)有自我拯救就會(huì)回收了。所以finalize()方法最好不要使用,太不可靠了,也不要想著用finalize()方法進(jìn)行自我拯救,finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時(shí)。
方法區(qū)回收
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類(lèi)型。回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常類(lèi)似。舉個(gè)常量池中字面量回收的例子,假如一個(gè)字符串“suncodernote”曾經(jīng)進(jìn)入常量池中,但是當(dāng)前系統(tǒng)又沒(méi)有任何一個(gè)字符串對(duì)象的值是“suncodernote”,換句話說(shuō),已經(jīng)沒(méi)有任何字符串對(duì)象引用常量池中的“suncodernote”常量,且虛擬機(jī)中也沒(méi)有其他地方引用這個(gè)字面量。如果在這時(shí)發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,這個(gè)“suncodernote”常量就將會(huì)被系統(tǒng)清理出常量池。常量池中其他類(lèi)(接口)、方法、字段的符號(hào)引用也與此類(lèi)似。
判定一個(gè)常量是否“廢棄”還是相對(duì)簡(jiǎn)單,而要判定一個(gè)類(lèi)型是否屬于“不再被使用的類(lèi)”的條件就比較苛刻了,必須同時(shí)滿足以下的條件(僅僅是可以,不代表必然,因?yàn)檫€有一些參數(shù)可以進(jìn)行控制):
- 該類(lèi)所有的實(shí)例都已經(jīng)被回收,也就是堆中不存在該類(lèi)的任何實(shí)例。
- 加載該類(lèi)的 ClassLoader 已經(jīng)被回收。
- 該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類(lèi)的方法.
- 參數(shù)控制:-Xnoclassgc參數(shù)可以禁用類(lèi)的垃圾收集(GC),這可以節(jié)省一些GC時(shí)間,從而縮短應(yīng)用程序運(yùn)行期間的中斷
到此這篇關(guān)于java中如何判斷對(duì)象是否是垃圾的文章就介紹到這了,更多相關(guān)java判斷垃圾內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot開(kāi)啟mybatis駝峰命名自動(dòng)映射的三種方式
這篇文章給大家總結(jié)springboot開(kāi)啟mybatis駝峰命名自動(dòng)映射的三種方式,文章并通過(guò)代碼示例給大家介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2024-02-02
SpringBoot+SpringSecurity處理Ajax登錄請(qǐng)求問(wèn)題(推薦)
這篇文章主要介紹了SpringBoot+SpringSecurity處理Ajax登錄請(qǐng)求問(wèn)題,本文給大家介紹的非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-12-12
SpringBoot3.3.X整合Mybatis-Plus的實(shí)現(xiàn)示例
本文介紹了在Spring Boot 3.3.2中整合MyBatis-Plus 3.5.7,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
java實(shí)現(xiàn)簡(jiǎn)易版簡(jiǎn)易版dubbo
dubbo是阿里開(kāi)源的rpc框架,目前是apache頂級(jí)開(kāi)源項(xiàng)目,可以用來(lái)構(gòu)建微服務(wù)。本文主要介紹了如何通過(guò)java實(shí)現(xiàn)簡(jiǎn)易版的dubbo,感興趣的小伙伴可以了解一下2021-11-11
Java實(shí)現(xiàn)根據(jù)模板讀取PDF并替換指定內(nèi)容
在實(shí)際開(kāi)發(fā)里,經(jīng)常會(huì)遇到需要根據(jù)?PDF?模板文檔生成特定?PDF?的需求,本文將利用Java中的iText實(shí)現(xiàn)讀取?PDF?模板文檔并替換指定內(nèi)容,最后重新生成新PDF,感興趣的可以了解下2025-02-02
SpringBoot框架實(shí)現(xiàn)切換啟動(dòng)開(kāi)發(fā)環(huán)境和測(cè)試環(huán)境
這篇文章主要介紹了SpringBoot框架實(shí)現(xiàn)切換啟動(dòng)開(kāi)發(fā)環(huán)境和測(cè)試環(huán)境,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12

