跟面试官聊到JVM,他99%会让你谈谈这个问题!
本文轉(zhuǎn)載自微信公眾號(hào):王磊的博客
但凡問到 JVM(Java 虛擬機(jī))通常有 99% 的概率一定會(huì)問:在 JVM 中如何判斷一個(gè)對(duì)象的生死狀態(tài)?
本文就來聊聊這個(gè)問題,判斷對(duì)象的生死狀態(tài)的算法有以下幾個(gè):
1、引用計(jì)數(shù)器算法
引用計(jì)算器判斷對(duì)象是否存活的算法是這樣的:給每一個(gè)對(duì)象設(shè)置一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用這個(gè)對(duì)象的時(shí)候,計(jì)數(shù)器就加1,與之相反,每當(dāng)引用失效的時(shí)候就減1。
優(yōu)點(diǎn):實(shí)現(xiàn)簡單、性能高。
缺點(diǎn):增減處理頻繁消耗cpu計(jì)算、計(jì)數(shù)器占用很多位浪費(fèi)空間、最重要的缺點(diǎn)是無法解決循環(huán)引用的問題。
因?yàn)橐糜?jì)數(shù)器算法很難解決循環(huán)引用的問題,所以主流的Java虛擬機(jī)都沒有使用引用計(jì)數(shù)器算法來管理內(nèi)存。
public class ReferenceDemo {public Object instance = null;private static final int _1Mb = 1024 * 1024;private byte[] bigSize = new byte[10 * _1Mb]; // 申請(qǐng)內(nèi)存public static void main(String[] args) {System.out.println(String.format("開始:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));ReferenceDemo referenceDemo = new ReferenceDemo();ReferenceDemo referenceDemo2 = new ReferenceDemo();referenceDemo.instance = referenceDemo2;referenceDemo2.instance = referenceDemo;System.out.println(String.format("運(yùn)行:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));referenceDemo = null;referenceDemo2 = null;System.gc(); // 手動(dòng)觸發(fā)垃圾回收System.out.println(String.format("結(jié)束:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));}}運(yùn)行的結(jié)果:
開始:117 M
運(yùn)行中:96 M
結(jié)束:119 M
從結(jié)果可以看出,虛擬機(jī)并沒有因?yàn)橄嗷ヒ镁筒换厥账鼈?#xff0c;也側(cè)面說明了虛擬機(jī)并不是使用引用計(jì)數(shù)器實(shí)現(xiàn)的。
2、可達(dá)性分析算法
在主流的語言的主流實(shí)現(xiàn)中,比如Java、C#、甚至是古老的Lisp都是使用的可達(dá)性分析算法來判斷對(duì)象是否存活的。
這個(gè)算法的核心思路就是通過一些列的“GC Roots”對(duì)象作為起始點(diǎn),從這些對(duì)象開始往下搜索,搜索所經(jīng)過的路徑稱之為“引用鏈”。
當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連的時(shí)候,證明此對(duì)象是可以被回收的。如下圖所示:
在Java中,可作為GC Roots對(duì)象的列表:
-
Java虛擬機(jī)棧中的引用對(duì)象。
-
本地方法棧中JNI(既一般說的Native方法)引用的對(duì)象。
-
方法區(qū)中類靜態(tài)常量的引用對(duì)象。
-
方法區(qū)中常量的引用對(duì)象。
對(duì)象生死與引用的關(guān)系
從上面的兩種算法來看,不管是引用計(jì)數(shù)法還是可達(dá)性分析算法都與對(duì)象的“引用”有關(guān),這說明:對(duì)象的引用決定了對(duì)象的生死。
那對(duì)象的引用都有那些呢?
在JDK1.2之前,引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一塊引用。
這樣的定義很純粹,但是也很狹隘,這種情況下一個(gè)對(duì)象要么被引用,要么沒引用,對(duì)于介于兩者之間的對(duì)象顯得無能為力。
JDK1.2之后對(duì)引用進(jìn)行了擴(kuò)充,將引用分為:
-
強(qiáng)引用(Strong Reference)
-
軟引用(Soft Reference)
-
弱引用(Weak Reference)
-
虛引用(Phantom Reference)
這也就是文章開頭第一個(gè)問題的答案,對(duì)象不是非生即死的,當(dāng)空間還足夠時(shí),還可以保留這些對(duì)象
如果空間不足時(shí),再拋棄這些對(duì)象。很多緩存功能的實(shí)現(xiàn)也符合這樣的場(chǎng)景。
強(qiáng)引用、軟引用、弱引用、虛引用,這4種引用的強(qiáng)度是依次遞減的。
強(qiáng)引用:在代碼中普遍存在的,類似“Object obj = new Object()”這類引用,只要強(qiáng)引用還在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
軟引用:是一種相對(duì)強(qiáng)引用弱化一些的引用,可以讓對(duì)象豁免一些垃圾收集,只有當(dāng)jvm認(rèn)為內(nèi)存不足時(shí),才會(huì)去試圖回收軟引用指向的對(duì)象。jvm會(huì)確保在拋出OutOfMemoryError之前,清理軟引用指向的對(duì)象。
弱引用:非必需對(duì)象,但它的強(qiáng)度比軟引用更弱,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。
虛引用:也稱為幽靈引用或幻影引用,是最弱的一種引用關(guān)系,無法通過虛引用來獲取一個(gè)對(duì)象實(shí)例,為對(duì)象設(shè)置虛引用的目的只有一個(gè),就是當(dāng)著個(gè)對(duì)象被收集器回收時(shí)收到一條系統(tǒng)通知。
死亡標(biāo)記與拯救
在可達(dá)性算法中不可達(dá)的對(duì)象,并不是“非死不可”的,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記的過程。
如果對(duì)象在進(jìn)行可達(dá)性分析之后,沒有與GC Roots相連接的引用鏈,它會(huì)被第一次標(biāo)記,并進(jìn)行篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。
執(zhí)行finalize()方法的兩個(gè)條件:
1、重寫了finalize()方法。
2、finalize()方法之前沒被調(diào)用過,因?yàn)閷?duì)象的finalize()方法只能被執(zhí)行一次。
如果滿足以上兩個(gè)條件,這個(gè)對(duì)象將會(huì)放置在F-Queue的隊(duì)列之中,并在稍后由一個(gè)虛擬機(jī)自建的、低優(yōu)先級(jí)Finalizer線程來執(zhí)行它。
對(duì)象的“自我拯救”
finalize()方法是對(duì)象脫離死亡命運(yùn)最后的機(jī)會(huì),如果對(duì)象在finalize()方法中重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,比如把自己(this關(guān)鍵字)賦值給某個(gè)類變量或?qū)ο蟮某蓡T變量。
來看具體的實(shí)現(xiàn)代碼:
public class FinalizeDemo {public static FinalizeDemo Hook = null;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("執(zhí)行finalize方法");FinalizeDemo.Hook = this;}public static void main(String[] args) throws InterruptedException {Hook = new FinalizeDemo();// 第一次拯救Hook = null;System.gc();Thread.sleep(500); // 等待finalize執(zhí)行if (Hook != null) {System.out.println("我還活著");} else {System.out.println("我已經(jīng)死了");}// 第二次,代碼完全一樣Hook = null;System.gc();Thread.sleep(500); // 等待finalize執(zhí)行if (Hook != null) {System.out.println("我還活著");} else {System.out.println("我已經(jīng)死了");}}}執(zhí)行的結(jié)果:
執(zhí)行finalize方法
我還活著
我已經(jīng)死了
從結(jié)果可以看出,任何對(duì)象的finalize()方法都只會(huì)被系統(tǒng)調(diào)用一次。
不建議使用finalize()方法來拯救對(duì)象,原因如下:
1、對(duì)象的finalize()只能執(zhí)行一次。
2、它的運(yùn)行代價(jià)高昂。
3、不確定性大。
4、無法保證各個(gè)對(duì)象的調(diào)用順序。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的跟面试官聊到JVM,他99%会让你谈谈这个问题!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 动态代理及 RPC 框架介绍
- 下一篇: 彻底吃透Web服务器、容器、应用程序服务