垃圾收集器與內存分配策略(一)

前言:

好像蠻久沒有更新《深入理解jvm》這本書了,又回來填坑了。

垃圾收集器與內存分配策略。

程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅。這幾個區域的內存分配和回收都具備確定性,在這幾個區域基本不需要考慮回收問題。因爲方法結束或者線程結束,內存自然就回收了。而java堆和方法區則不一樣。方法區存放類信息,一個接口中的多個實現類需要的內存不一樣,並且只有在程序運行期間才知道會創建哪些對象。這部分內存的分配和回收都是動態的,所以垃圾回收器所關注的就是java堆和方法區。

如何判斷對象已死

引用計數算法

給對象添加一個引用計數器,每當有一個地方引用它,計數器值+1,當引用失效,計數器值-1。任何計數器爲0的對象就是不可能再被使用。

不過java不是用此算法,因爲此算法難以解決對象之間相互循環引用問題。

例如:

public class Person {
    public Object instance=null;

    public static void main(String[] args) {
        Person personA=new Person();
        Person personB=new Person();
        personA.instance=personB;
        personB.instance=personA;
        personA=null;
        personB=null;
        System.gc();
    }
}

看上方代碼可以開出對象personA和personB都有字段instance,並且相互引用,但是這兩個對象已經不可能再被訪問了,但是他們因爲互相引用着對方,導致引用計數器不爲0,於是引用計數算法無法通知gc收集器回收他們。

可達性分析算法

此算法的基本思想是通過一系列稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

作爲GC Roots對象包括:

  • 虛擬機棧(棧幀中的局部變量表)中的引用對象
  • 方法區中類靜態屬性引用的對象(靜態變量)
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Java Navice Interface 即一般說的native方法)引用的對象。

引用

定義:

如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。

引用分爲四類:

  • 強引用

    是指代碼中普遍存在的,類似“Object obj=new Object()”這一類的引用,只要強引用還在,垃圾收集器就永遠不會回收掉被引用的對象。

  • 軟引用

    用來描述一些還有用但並非必需的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列出回收範圍之中進行第二次回收。SolftReference類實現軟引用

  • 弱引用

    用來描述非必需對象,它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。WeakReference類來實現弱引用

  • 虛引用

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

對象死亡過程

需要經歷兩次標記過程:

  • 一、如果一個對象在經過可達性分析後發現沒有與GC Roots相連接的引用鏈,那麼它將會被第一次標記並且進行一次篩選。篩選條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize方法或者finalize方法已經被虛擬機調用過,虛擬機都將這兩種情況是爲“沒有必要執行”
  • 二、如果這個對象被判定有必要執行finalize()方法,那麼這個對象將會放置在F-Queue的隊列之中,並稍後有一個虛擬機自動建立的、低優先級的finalizer線程去執行。finalise方法是對象最後一次拯救自己的機會,稍後gc會對f-queue中的對象進行第二次小規模標記,如果對象要在finalize中成功拯救自己—需要重新與引用鏈上的人和一個對象建立關聯即可(例如把自己賦值給某個類變量或者對象的成員變量),那麼在第二次標記的時候就會移出集合;如果此時對象還沒有逃脫,那麼基本上就真的被回收了。

流程:一個對象沒有與gc roots相連接的引用鏈-------->篩選有無執行過finalize()--------->有則被放置在F-Queue的隊列,無則直接回收------->虛擬機執行finalize()方法(對象可在此處重新建立連接逃脫回收)------->對F-Queue隊列中對象進行二次標記-------->ggggggg 垃圾回收了

finalize()有一個特點就是: JVM始終只調用一次. 無論這個對象被垃圾回收器標記爲什麼狀態, finalize()始終只調用一次. 但是程序員在代碼中主動調用的不記錄在這之內.

回收方法區

永生代的垃圾收集主要回收兩個部分:廢棄常量和無用的類。

回收廢棄常量:回收廢棄常量和回收java堆中的對象非常相似,例如string,一個字符串“abc”已經進入了常量池,當前系統中沒有任何一個string對象叫做“abc”,即沒有任何地方引用這個字符串。如果發生內存回收,這個“abc”就會被系統清理出常量池。

回收無用的類:需要滿足三個條件

  • 該類的所有實例都已經被回收,也就是java堆中不存在該類的任何實例
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。(通常在對象中保存有,或者一些反射的場景中有)

垃手收集算法

注意:先說明一下新生代、老年代、永生代的概念

堆中分爲新生代–一般採用複製算法 和老年代-一般採用標記整理算法、標記清除算法

而方法區纔是永生代。

從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC。 Major GC 是清理永久代。Full GC 是清理整個堆空間—包括年輕代和永久代。

標記-清除算法

分爲標記和清除兩個階段,首先標記出所有需要回收的對象。在標記完成後統一回收所有被標記的對象,標記判定:棧中才有可達性分析、方法區則又分成兩類廢棄常量和無用類。

不足之處:標記和清除的效率不高,還有就是空間問題,標記清楚後會產生大量不連續的內存碎片,如果太多會導致程序運行期間分配大對象的時候,無法找到足夠的連續的內存空間份額耦,而觸發一次垃圾回收動作。

複製算法

將可用的容量劃分爲2,每次只使用其中的一塊,當一塊內存用完的時候,就將還存活着的對象複製到另外一塊上。複製的時候由於是按順序內存分配,所有不會產生不連續的內存空間。

不足之處:1、內存縮小了一半,可用空間少了一半啊。2、當對象的存活率高的時候,ggg了太多的複製操作,效率低。

實際上新生代的垃圾收集器內部的實現方式就是複製算法,但是它們進行了改進,是將內存分爲一塊較大的Eden和兩塊較小的Survivor。Survival區有兩塊,一塊稱爲from區,另一塊爲to區,這兩個區是相對的,在發生一次Minor GC後,from區就會和to區互換。每次使用的話只使用eden加一塊Survivor。當回收的時候,將Eden和Survivor from中的存活對象複製到另一塊Survivor to空間上,最後清除掉Eden和剛剛使用過的Survivor from,然後後面使用的就是存放着活着的對象的Survivor to(改名爲Survivor from)和Eden,此時Survivor to中存活着的對象age+1,Survival to區會把一些存活得足夠舊的對象移至年老代。(這部分與對象由新生代轉換爲老年代有關,後面會說到)。

虛擬機中Eden和Survivor的大小佔比是8:1,所以每次新生代中可用的內存就是90%,至於爲什麼Survivor只佔1,那是因爲新生代中的對象大多數都不會存活太久,用完就可以清理的,所以理論是1/10的內存空間即可存放存活的新生代對象。

但是凡事也是有例外的:

當存活的對象太多了,Survivto to的空間不夠用的時候,這時候就需要依賴其他內存(老年代)進行分配擔保。過程就是Survivto from 和 Eden中生存下來的對象太多了,Survivto to無法存放這麼多的對象,此時這部分的對象通過分配擔保機制進入老年代。分配擔保機制後面再細說。

下圖是別的博客處拿的圖:根據來比較好理解我上述的話。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yN3b52io-1589450484533)(/Users/awakeyoyoyo/Library/Application Support/typora-user-images/image-20200514134150795.png)]

標記-整理算法

標記過程和標記清除算法一樣,但是後續的步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動沒然後清理掉端邊界以外的內存。

分代收集算法

其實就是將堆分爲新生代和老年代,不同的年代特點採取最合適的收集算法,新生代由於對象存活率低,採用複製算法,老年代由於存活率高採用標誌清除或者標誌整理算法。 這些取決於垃圾收集器的實現。

HotSpot算法實現
枚舉根節點:

由於可達性分析需要從GC Roots節點中尋找引用鏈。這是就需要用到枚舉根節點了,把所有GC Roots枚舉出來。

GC Roots中一般爲全局性引用(常量、類靜態屬性)和執行上下文(棧幀中的本地變量表),枚舉出這個GC Roots我們需要考慮到這個分析過程所產生結果的準確性枚舉效率,也就是我們此時要講的保證“一致性”快照和提高枚舉效率。

如何提高枚舉效率:OOPMap

但執行系統停頓下來後,不需要一個不漏的檢查完所有執行上下文和全局引用的位置,使用OOPMap的數據結構,來讓虛擬機得知哪些地方存放着對象的引用。

一個線程意味着一個棧,一個棧由多個棧幀組成,一個棧幀對應着一個方法,一個方法裏面可能有多個安全點。 gc 發生時,程序首先運行到最近的一個安全點停下來,然後更新自己的 OopMap ,記下棧上哪些位置代表着引用。枚舉根節點時,遞歸遍歷每個棧幀的 OopMap ,通過棧中記錄的被引用對象的內存地址,即可找到這些對象( GC Roots )。使用 OopMap 可以避免全棧掃描,加快枚舉根節點的速度。

那麼如何保證一致性快照呢:GC停頓
  • GC停頓

    即在整個分析期間整個執行系統,不可以出現分析過程中對象引用關係還在不斷變化的過程,否則分析結果的準確性就會無法得到保證。所以程序需要在此時停止下來。

如何保證每次gc停頓的位置都有OOPMap記錄下對象的引用位置:安全點、安全區域
  • 安全點

    1、何爲安全點

    我們提到要提高枚舉效率就要用到OOPMap來存儲引用的位置,但是如果爲每一條指令的生成對應的OOPMap就會需要大量的額外空間,虛擬機只在特定的位置記錄了OOPMap信息,這些位置稱爲安全點。程序在執行時,並非在所有的地方都能停下來開始GC,只有到達這個“安全點“時才能停頓下來。安全點的選區既不能太少以至於讓GC等待時間過長,也不能過於頻繁以至於過分增大運行時的負荷。安全點的選定基本上是成語“是否具有讓程序長時間執行的特徵”爲標準來進行選定的,

    2、如何讓所有線程停止

    搶先式中斷”不需要線程的執行代碼去主動配合,在GC發生時,首次會把所有的線程全部中斷,如果發現有些線程中斷點不是安全點,就恢復該線程直到安全點上停止。

    ”主動式中斷“實際上就是線程主動輪詢的一個過程,當GC需要中斷線程時,不直接對線程進行操作,僅僅簡單的設置一個標誌,這個輪詢標誌當然要與安全點相重合。各個線程在執行的時候都會主動去詢問這個輪詢標誌:”我是否到了該中斷的點了?“。如果是真,就把線程掛起,現在大部分虛擬機都採用是”主動式中斷”方式,因爲它相對“搶先式中斷”方式避免了一箇中斷——>啓動——>又中斷的一個過程。

  • 安全區域

    設置“安全點”雖然保證了大部分線程停頓,但總有例外,假如某些線程正在阻塞活着睡眠。請求中斷時,JVM不可能等該線程睡醒之後到達安全點之後才能進行可達性分析過程,而此時如果該線程睡醒了恰巧GC又在進行可達性分析或者是回收,那該線程又該何去何從呢?所以JVM又在安全點的基礎上加了一個雙重保險——安全區域。

    安全區域是指在一段代碼片中,引用關係不會發生改變,實際上就是一個安全點的拓展。當線程執行到安全區域時,首先標識自己已進入安全區域,那樣,當在這段時間裏JVM要發起GC時,就不用管標識自己爲“安全區域”狀態的線程了,該線程只能乖乖的等待根節點枚舉或者整個GC過程完成之後才能繼續執行。當然如果沒進安全區域的線程,還是要等待他進行安全點的

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