Java虛擬機(5)對象存活及引用&垃圾回收方法 對象的存活與死亡 垃圾回收算法

對象的存活與死亡

堆裏面存放着Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”着,哪些已經“死去”。

如何確定對象是否存活?

從如何判定對象消亡的角度出發,垃圾收集算法可分爲“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC),也常被稱作“直接垃圾收集”和“間接垃圾收集”。

1、引用計數法

定義:在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器爲零的對象就是不可能再被使用的。

缺陷:單純的引用計數就很難解決對象之間相互循環引用的問題

2、可達性分析算法

定義:通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,即從GC Roots到這個對象不可達,則證明此對象是不可能再被使用的。

在Java技術體系裏面,固定可作爲GC Roots的對象包括以下幾種:

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。

  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。

  • 在方法區中常量引用的對象,譬如字符串常量池(StringTable)裏的引用。

  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。

  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。

  • 所有被同步鎖(synchronized關鍵字)持有的對象。

  • 反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

對象引用

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判定對象是否存活都和”引用”離不開關係。

引用關係可分爲以下幾個大類:

  • 強引用

    普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關係。只要強引用關係存在,垃圾收集器就永遠不會回收掉被引用的對象。

  • 軟引用

    一些還有用,但非必須的對象。發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,回收後還沒有足夠的內存,纔會拋出內存溢出異常。 SoftReference類實現。

  • 弱引用

    非必須對象,強度比軟引用更弱一些,只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工作,無論當前內存是否足夠,都會被回收掉。 WeakReference類實現。

  • 虛引用

    最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。唯一目的是在這個對象被收集器回收時收到一個系統通知。PhantomReference類實現。

對象死亡過程

要真正宣告一個對象死亡,至少要經歷兩次標記過程:

1、第一次標記、篩選

對象可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記;

篩選:判斷對象是否有必要執行finalize()方法。

  • 有必要執行:對象將會被放置F-Queue的隊列,由虛擬機自動建立的、低調度優先級的Finalizer線程去執行finalize()方法;
  • 沒必要執行:對象死亡,等待回收。

對象沒有finalize()方法,或finalize()方法已被調用過,視爲“沒有必要執行”。

2、第二次標記、篩選

收集器將對F-Queue中的對象進行第二次標記;

  • 對象要在finalize()中——重新與引用鏈上的任何一個對象建立關聯,如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,在第二次標記時它將被移出“即將回收”的集合;
  • 沒有建立關聯,對象死亡,等待回收。

finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,不建議使用這個方法。

回收方法區

方法區的垃圾收集主要包括:廢棄的常量和不再使用的類型。

廢棄常量:沒有任何字符串對象引用常量池中的常量,且虛擬機中也沒有地方引用這個字面量。

不再被使用的類,需要同時滿足三個條件:

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例;

  • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的;

  • 類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。

垃圾回收算法

從如何判定對象消亡的角度出發,垃圾收集算法可以劃分爲“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”。引用計數式垃圾收集算法主流Java虛擬機中未涉及,以下介紹的所有算法均屬於追蹤式垃圾收集的範疇。

分代手機理論

兩個分代假說:

1、弱分代假說:絕大多數對象都是朝生夕滅的。

2、強分代假說:熬過越多次垃圾手機過程的對象越難以消亡。

垃圾收集器設計原則:

將Java堆劃分出不同的區域,將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。

具體到商用Java虛擬機裏,設計者一般至少會把Java堆劃分爲新生代(Young Generation)和老年代(Old Generation)兩個區域。在新生代中,每次垃圾收集時都有大批對象死去,每次回收後存活的少量對象,將會逐步晉升到老年代中存放

3、跨代引用假說:跨代引用相對於同代引用來說僅佔少數。

根據前兩條假說邏輯推理得出的隱含推論:存在互相引用關係的兩個對象,應該傾向於同時生存或者同時消亡。

垃圾回收算法

1、標記清除算法

最基礎的收集算法

標記出所有需要回收的對象,標記完成後,統一回收掉所有被標記的對象;也可反過來,標記存活的對象,統一回收所有未被標記的對象。

缺陷

  • 執行效率不穩定:如果Java堆中包含大量對象,且其中大部分是要被回收的,就必須進行大量標記和清除的動作,執行效率都隨對象數量增長而降低;
  • 內存空間的碎片化問題:標記、清除之後會產生大量不連續的內存碎片,空間碎片太多會導致當需要分配較大對象時,無法找到足夠的連續內存,不得不提前觸發另一次垃圾收集。

2、標記-複製算法

將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

優勢

多數對象都是可回收的情況,複製的就是佔少數的存活對象,每次都是針對整個半區進行內存回收,分配內存時不用考慮有空間碎片的複雜情況,只需移動堆頂指針,按順序分配。

缺點

  • 內存中多數對象都是存活時,將會產生大量的內存間複製的開銷;
  • 而且將可用內存縮小爲了原來的一半,空間浪費。

使用場景

適用於對象存活率較低,需要頻繁進行垃圾回收的區域。

HotSpot虛擬機的Serial、ParNew等新生代收集器均採用了這種策略來設計新生代的內存佈局。

3、標記-整理算法

標記過程仍然與“標記-清除”算法一樣,但後續不是直接對可回收對象進行清理,而是所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。

使用場景

適用於對象存活率較低,垃圾回收行爲頻率低的場景。

HotSpot虛擬機裏面關注吞吐量的Parallel Scavenge收集器是基於標記-整理算法的,而關注延遲的CMS收集器則是基於標記-清除算法。

4、分代收集算法

針對不同的年代進行不同算法的垃圾回收,針對新生代選擇複製算法,對老年代選擇標記整理算法。

以上就是java虛擬機對象存活、引用及垃圾回收方法垃圾回收的全部內容了,總結如下:

歡迎點贊/評論,你們的贊同和鼓勵是我寫作的最大動力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章