深刻理解JVM虛擬機-對象引用,GC與內存分配回收

1 對象的生存和死亡

在堆裏面存放着Java世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑使用的對象),那在GC是如何判斷一個對象是否存活仍是死亡呢?html

1.1 引用計數算法(Reference Counting)

不少教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。這就是引用計數算法(Reference Counting)。 引用計數算法(Reference Counting)的實現簡單,斷定效率也很高,但主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。java

相互循環引用即爲對象A中拿着對象B的引用,對象B中拿着對象A的引用。算法

1.2 可達性分析算法(Reachability Analysis)

在java中,是經過可達性分析(Reachability Analysis)來斷定對象是否存活的。這個算法的基本思路就是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。 如圖示,對象object 五、object 六、object 7雖然互相有關聯,可是它們到GC Roots是不可達 的,因此它們將會被斷定爲是可回收的對象。 安全

在這裏插入圖片描述
在Java語言中,可做爲GC Roots的對象包括下面幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  2. 方法區中類靜態屬性引用的對象。
  3. 方法區中常量引用的對象。
  4. 本地方法棧中JNI(即通常說的Native方法)引用的對象。

1.3 對象引用

在JDK1.2以前,Java中的引用的定義是若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態。 在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲** 強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference) 4種**,這4種引用強度依次逐漸減弱。bash

  1. 強引用:是指在程序代碼之中廣泛存在的,相似「Object obj=new Object()」這類的引 用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
  2. 軟引用:是用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將 要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回 收尚未足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了SoftReference類來實現軟引用。
  3. 弱引用:也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。在JDK 1.2以後,提供了WeakReference類來實現弱引用。
  4. 虛引用:也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引 用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一 個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在 JDK 1.2以後,提供了PhantomReference類來實現虛引用。

1.4 死亡與自救

真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,這兩種狀況下對象都會被回收。 若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的Finalizer線程去觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環,將極可能會致使F-Queue隊列中其餘對象永久處於等待,甚至致使整個內存回收系統崩潰。數據結構

package com.wtj.myjvm;

/**
 * Created on 2019/4/11.
 *
 * @author wangtingjun
 * @since 1.0.0
 */

/**
 * 1.對象能夠在被GC時自我拯救。
 * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
 *
 * @author wtj
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes,i am still alive:)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //對象第一次成功拯救本身
        SAVE_HOOK = null;
        System.gc();
        //由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
        //下面這段代碼與上面的徹底相同,可是此次自救卻失敗了
        SAVE_HOOK = null;
        System.gc();
        //由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
    }
}
複製代碼

運行結果:多線程

finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(
複製代碼

從上面的代碼能夠看出,對象的finalize()方法確實被GC收集器觸發過,而且在被收集前成功逃脫了。 對象的finalize()方法最多隻會被系統自動調用一次。併發

2 垃圾收集算法

2.1 標記-清除算法

標記-清除算法是最基礎的收集算法之一,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。 它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。jvm

2.2 複製算法

它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。 這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,運行高效。但代價是將內存縮小爲原來的二分之一。 如今的商業虛擬機都採用這種收集算法來回收新生代,研究代表,新生代中的對象98%是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存會被「浪費」。 可是沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴其餘內存進行分配擔保(Handle Promotion)。即若是另一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代ide

2.3 標記-整理算法(Mark-Compact)

標記-整理算法的標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

2.4 分代收集算法(Generational Collection)

當前商業虛擬機的垃圾收集都採用「分代收集」,是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆 分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記—清理」或者「標記—整理」算法來進行回收。

3 HotSpot虛擬機的算法

3.1 枚舉根節點OopMap

HotSpot虛擬機使用可達性分析算法來判斷一個對象的存活狀態,若是逐個檢查每一個GC Roots節點,勢必會消耗不少時間。而且這項分析工做必須在一個能確保一致性的快照中進行,即在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,該點不知足的話分析結果準確性就沒法獲得保證。這點是致使GC進行時必須停頓全部Java執行線程(Sun將這件事情稱爲「Stop The World」)的其中一個重要緣由。 在HotSpot的實現中,是使用一組稱爲OopMap的數據結構存放着對象引用,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就能夠直接得知這些信息了。

3.2 安全點 Safepoint

HotSpot經過OopMap能夠快速的完成根節點的枚舉,OopMap內容變化的指令很是多,若是爲每一 條指令都生成對應的OopMap,那將會須要大量的額外空間,這樣GC的空間成本將會變得很高。 實際上,HotSpot只是在「特定的位置」記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。 安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。

4 HotSpot使用的GC與內存分配回收

我使用的JDK版本是JDK1.8,經過命令java -XX:+PrintCommandLineFlags -version能夠查看到虛擬機默認使用的垃圾收集器。

在這裏插入圖片描述
從參數 -XX:+UseParallelGC能夠看出,默認使用的垃圾收集器是 Parallel Scavenge(新生代)+ Serial Old(老年代)

4.1 Parallel Scavenge

Parallel Scavenge收集器是一個新生代收集器,他使用的是複製算法,而且是多線程的收集器。Parallel Scavenge的目標是打到一個可控制的吞吐量,也就是CPU用於運行用戶代碼的時間與CPU總耗時時間的比, Parallel Scavenge收集器也被稱爲「吞吐量優先」收集器。 Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。也能夠經過參數-XX:+UseAdaptiveSizePolicy來開啓自適應的調節策略,虛擬機會根據當前系統的運行狀況收集性能監控信息進行動態分配。

4.2 Serial Old

Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用「標記-整理」算法。

4.3 CMS(Concurrent Mark Sweep)

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。 CMS收集器是基於「標記—清除」算法實現的,運做分爲四個步驟:初始標記(CMS initial mark)併發標記(CMS concurrent mark)從新標記(CMS remark)併發清除(CMS concurrent sweep)。其中併發標記和併發清除最爲耗時,但能夠與用戶線程一塊兒工做。

4.4 內存的分配與回收

java程序在運行的時候,會建立大量的對象,這些對象的生命週期可不相同,生命週期短的對象主要被分配在新生代的Eden(伊甸園)區上,少數狀況下會被分配在老年代,分配規則並不百分百固定,對於生命週期長的對象則被分配在老年代。 年輕代:是主要存放新建的對象,年輕代的垃圾回收比較頻繁。年輕代中分爲一個Eden區和兩個survior區(from和to),比例爲8:1。年輕代中的對象壽命較短,使用的是複製算法。 在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。

在這裏插入圖片描述
年老代:老年代,用於存放新生代中通過屢次垃圾回收仍然存活的對象,也有多是新生代分配不了內存的大對象會直接進入老年代。通過屢次垃圾回收且年齡達到 MaxTenuringThreshold的對象就會放入到老年代。 當老年代被放滿的以後,虛擬機會進行垃圾回收,稱之爲Major GC。因爲Major GC除併發GC外均需對整個堆進行掃描和回收,所以又稱爲Full GC, Full GC會致使程序的暫時中止,大量的Full GC會致使系統響應速度下降,並且引來巨大的風險Perm Gen(永久代) (JDK1.8以後被元空間替代):Perm Gen全稱是Permanent Generation space,稱之爲永久代, 其實就是這個方法區,只不過在HotSpot虛擬機中常被稱爲永久代。

年輕代和年老代得比例爲1:2

部分參考:www.cnblogs.com/haitaofeiya…

相關文章
相關標籤/搜索