Java gc中能聊的那些事

咱們已經知道Java堆是被全部線程共享的一塊內存區域,全部對象實例和數組都在堆棧進行內存分配。爲了進行高效的垃圾回收,虛擬機把堆內存劃分紅新生代年代(舊一代)和永久代(永久代)3個區域。html

新生代

新生代由Eden與Survivor Space(S0,S1)構成,大小經過-Xmn參數指定,Eden與Survivor Space的內存大小比例默認爲8:1,能夠經過-XX:SurvivorRatio參數指定,好比新生代爲10M時,伊甸園分配8M,S0和S1各分配1M。程序員

Eden:希臘語,意思爲伊甸園,在聖經中,伊甸園含有樂園的意思,根據「舊約·創世紀」記載,上帝耶和華照本身的形像造了第一個男人亞當,再用亞當的一個肋骨創造了一個女人夏娃,並安置他們住在了伊甸園。面試

大多數狀況下,對象在Eden中分配,當Eden沒有足夠空間時,會觸發一次Minor GC,虛擬機提供了-XX:+ PrintGCDetails參數,告訴虛擬機在發生垃圾回收時打印內存回收日誌。算法

Survivor:意思爲倖存者,是新生代和老年代的緩衝區域。
當新生代和老年代的緩衝區域。當新生代發生GC(Minor GC)時,會將存活的對象移動到S0內存區域,並清空Eden區域,當再次發生Minor GC時,將伊甸園和S0中存活的對象移動到S1內存區域。數組

存活對象會反覆在S0和S1之間移動,當對象從伊甸園移動到倖存者或者在倖存者之間移動時,對象的GC年齡自動累加,當GC年齡超過默認閾值15時,會將該對象移動到老年代,能夠經過參數-XX:MaxTenuringThreshold對GC年齡的閾值進行設置。安全

老年代

老年代的空間大小即-Xmx與-Xmn兩個參數之差,用於存放通過幾回Minor GC以後依舊存活的對象。當老年代的空間不足時,會觸發主GC / Full GC,速度通常比小GC慢10倍以上。性能優化

永久代

在JDK8以前的熱點實現中,類的元數據如方法數據,方法信息(字節碼,棧和變量大小),運行時常量池,已肯定的符號引用和虛方法表等被保存在永久代中,32位默認永久代的大小爲64M,64位默認爲85M,能夠經過參數-XX:MaxPermSize參數進行設置,一旦類的元數據超過了永久代大小,就會拋出OOM異常。數據結構

虛擬機團隊在JDK8的熱點中,把永久代從Java的堆中移除了,並把類的元數據直接保存在本地內存區域(堆外內存),稱之爲元空間。多線程

這樣作有什麼好處?架構

有經驗的同窗會發現,對永久代的調優過程很是困難,永久代的大小很難肯定,其中涉及到太多因素,如類的總數,常量池大小和方法數量等,並且永久代的數據可能會隨着每一次Full GC而發生移動。

而在JDK8中,類的元數據保存在本地內存中,元空間的最大可分配空間就是系統可用內存空間,能夠避免永久代的內存溢出問題,不過須要監控內存的消耗狀況,一旦發生內存泄漏,會佔用大量的本地內存。

ps:JDK7以前的HotSpot,字符串常量池的字符串被存儲在永久代中,所以可能致使一系列的性能問題和內存溢出錯誤。在JDK8中,字符串常量池中只有保存字符串的引用。

如何判斷對象是否存活

GC動做發生以前,須要肯定堆內存中哪些對象是存活的,通常有兩種方法:引用計數法和可達性分析法。

1,引用計數法

在對象上添加一個引用計數器,每當有一個對象引用它時,計數器加1,當使用完該對象時,計數器減1,計數器值爲0的對象表示不可能再被使用。

引用計數法實現簡單,斷定高效,但不能解決對象之間相互引用的問題。

public class GCtest {
    private Object instance = null;
    private static final int _10M = 10 * 1 << 20;
    // 一個對象佔10M,方便在GC日誌中看出是否被回收
    private byte[] bigSize = new byte[_10M];

    public static void main(String[] args) {
        GCtest objA = new GCtest();
        GCtest objB = new GCtest();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();
    }
}

經過添加-XX:+PrintGC參數,運行結果:

[GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]

GC日誌中能夠看出objA和objB雖然相互引用,可是它們所佔的內存仍是被垃圾收集器回收了。

2,可達性分析法

經過一系列稱爲「GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索路徑稱爲「引用鏈」,如下對象可能爲GC Roots:

  • 本地變量表中引用的對象
  • 方法區中靜態變量引用的對象
  • 方法區中常量引用的對象
  • 本機方法引用的對象

當一個對象到GC Roots沒有任何引用連接,意味着該對象能夠被回收。

圖片

在可達性分析法中,斷定一個對象objA是否可回收,至少要經歷兩次標記過程:

1,若是對象objA到GC Roots沒有引用鏈,則進行第一次標記
。2,若是對象objA重寫了finalize()方法,而且還未執行過,那麼objA會被插入到F隊列隊列中,由一個虛擬機自動建立的,低優先級的終結線程觸發其的finalize()方法.finalize()方法是對象逃脫死亡的最後機會,GC會對隊列中的對象進行第二次標記,若是objA在敲定()方法中與引用鏈上的任何一個對象創建聯繫,那麼在第二次標記時,objA會被移出「即將回收」集合。

看看具體實現

public class FinalizerTest {
    public static FinalizerTest object;
    public void isAlive() {
        System.out.println("I'm alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("method finalize is running");
        object = this;
    }

    public static void main(String[] args) throws Exception {
        object = new FinalizerTest();

        // 第一次執行,finalize方法會自救
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }

        // 第二次執行,finalize方法已經執行過
        object = null;
        System.gc();

        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }
    }
}

執行結果:

method finalize is running
I'm alive
I'm dead

從執行結果能夠看出:

第一次發生GC時,最終化方法的確執行了,而且在被回收以前成功逃脫; 
第二次發生GC時,因爲最終化方法只會被JVM調用一次,對象被回收。

固然了,在實際項目中應該儘可能避免使用的finalize方法。

收集算法

垃圾收集算法主要有:標記 - 清除,複製和標記 - 整理。

1,標記 - 清除算法

。回收對待的對象進行標記
算法缺點:效率問題,標記和清除過程效率都很低;空間問題,收集以後會產生大量的內存碎片,不利於大對象的分配。

2,複製算法

複製算法將可用內存劃分紅大小相等的兩塊甲和B,每次只使用其中一塊,當甲的內存用完了,就把存活的對象複製到B,並清空甲的內存,不只提升了標記的效率,由於只須要標記存活的對象,同時也避免了內存碎片的問題,代價是可用內存縮小爲原來的一半。

3,標記 - 整理算法

在老年代中,對象存活率較高,複製算法的效率很低在標記 - 整理算法中,標記出全部存活的對象,並移動到一端,而後直接清理邊界之外的內存。

對象標記過程

在可達性分析過程當中,爲了準確找出與GC Roots相關聯的對象,必需要求整個執行引擎看起來像是被凍結在某個時間點上,即暫停全部運行中的線程,不能夠出現對象的引用關係還在不斷變化的狀況。

如何快速枚舉GC Roots?

GC Roots主要在全局性的引用(常量或類靜態屬性)與執行上下文(本地變量表中的引用)中,不少應用僅僅方法區就上百兆,若是進行遍歷查找,效率會很是低下。

在熱點中,使用一組稱爲OopMap的數據結構進行實現。類加載完成時,熱點把對象內什麼偏移量上是什麼類型的數據計算出來存儲到OopMap中,經過JIT編譯出來的本地代碼,也會記錄下棧和寄存器中哪些位置是引用.GC發生時,經過掃描OopMap的數據就能夠快速標識出存活的對象。

如何安全的GC?

線程運行時,只有在到達安全點(Safe Point)才能停頓下來進行GC。

基於OopMap的數據結構,HotSpot能夠快速完成GC Roots的遍歷,不過,Hotspot並不會爲每條指令都生成對應的OopMap,只會在安全點處記錄這些信息。

因此若是太多,可能致使運行時的性能問題。若是太多可能致使GC等待的時間太長,若是太頻繁可能致使運行時的性能問題。大部分指令的執行時間都很是短暫,一般會選擇一些執行時間較長的指令做爲安全點,如方法調用,循環跳轉和異常跳轉等。

發生GC時,如何讓全部線程跑到最近的安全點再暫停?

當發生GC時,不直對線程進行中斷操做,而是簡單的設置一箇中斷標誌,每一個線程運行到安全點的時候,主動去輪詢這個中斷標誌,若是中斷標誌爲真,則將本身進行中斷掛起。

這裏忽略了一個問題,當發生GC時,運行中的線程能夠跑到安全點後進行掛起,而那些處於睡眠或阻塞狀態的線程在此時沒法響應JVM的中斷請求,沒法到安全點處進行掛起,針對這種狀況,可使用安全區域(Safe Region)進行解決。

Safe Region是指在一段代碼片斷中,對象的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。

1,當線程運行到Safe Region的代碼時,首標標識已經進入了安全區域,若是這段時間內發生GC,JVM會忽略標識爲安全區域狀態的線程; 
2,當線程即將離開安全區域時,會檢查JVM是否已經完成GC,若是完成了,則繼續運行,不然線程必須等待直到收到能夠安全離開Safe Region的信號爲止;

垃圾收集器

Java的虛擬機規範並無規定垃圾收集器應該如何實現,用戶能夠根據系統特色對各個區域所使用的收集器進行組合使用。

上圖展現了7種不一樣分代的收集器,若是兩兩之間存在連線,說明能夠組合使用。

如圖1所示,串行收集器(串行GC)

Serial是一個採用單線程而且基於複製算法工做在新生代的收集器,進行垃圾收集時,必須暫停其餘全部的工做線程。對於單CPU環境來講,Serial因爲沒有線程交互的開銷,能夠很高效的進行垃圾收集動做,是客戶端模式下新生代默認的收集器。

2,ParNew收集器(並行GC)

ParNew實際上是串行的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲與串行同樣。

3,Parallel Scavenge收集器(並行回收GC)

Parallel Scavenge是一個採用多線程基礎複製算法並工做在新生代的收集器,其關注點在於達到一個可控的吞吐量,常常被稱爲「吞吐量優先」的收集器。

吞吐量=用戶代碼運行時間/(用戶代碼運行時間+垃圾收集時間)

Parallel Scavenge提供了兩個參數用於精確控制吞吐量:
1,-XX:MaxGCPauseMillis設置垃圾收集的最大停頓時間
2,-XX:GCTimeRatio設置吞吐量大小

4,Serial Old收集器(串行GC)

Serial Old是一個採用單線程基於標記 - 整理算法並工做在老年人的收集器,是客戶端模式下老年人默認的收集器。

5,Parallel Old收集器(並行GC)

Parallel Old是一個採用多線程基於標記 - 整理算法並工做在老年的收集器。在注重吞吐量以及CPU資源敏感的場合,能夠優先考慮Parallel Scavenge和Parallel Old的收集器組合。

6,CMS收集器(併發GC)

CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間爲目標的收集器,工做於老年代,基於「標記 - 清除」算法實現,整個過程分爲如下4步:

1,初始標記:這個過程只是標記如下GC Roots可以直接關聯的對象,可是仍然會中止世界; 
2,併發標記:進行GC Roots Tracing的過程,能夠和用戶線程一塊兒工做
。3,從新標記:用在修正併發標記期間因爲用戶程序繼續運行而致使標記產生變更的那部分記錄,這個過程會暫停全部線程,但其停頓時間遠比並發標記的時間短; 
4,併發清理:能夠和用戶線程一塊兒工做。

CMS收集器的缺點:

1,對CPU資源比較敏感,在併發階段,雖然不會致使用戶線程停頓,可是會佔用一部分線程資源,下降系統的總吞吐量。

2,沒法處理浮動垃圾,在併發清理階段,用戶線程的運行依然會產生新的垃圾對象,這部分垃圾只能在下一次GC時收集。

3,CMS是基於標記-清除算法實現的,意味着收集結束後會形成大量的內存碎片,可能致使出現老年代剩餘空間很大,卻沒法找到足夠大的連續空間分配當前對象,不得不提早觸發一次Full GC。

JDK1.5實現中,當老年代空間使用率達到68%時,就會觸發CMS收集器,若是應用中老年代增加不是太快,能夠經過-XX:CMSInitiatingOccupancyFraction參數提升觸發百分比,從而下降內存回收次數提升系統性能。

JDK1.6實現中,觸發CMS收集器的閾值已經提高到92%,要是CMS運行期間預留的內存沒法知足用戶線程須要,會出現一次「併發模式失敗」失敗,這是虛擬機會啓動Serial Old收集器對老年代進行垃圾收集,固然,這樣應用的停頓時間就更長了,因此這個閾值也不能設置的過高,若是致使了「併發模式失敗」失敗,反而會下降性能,至於如何設置這個閾值,還得長時間的對老年代空間的使用狀況進行監控。

7,G1收集器

G1(Garbage First)是JDK1.7提供的一個工做在新生代和老年代的收集器,基於「標記 - 整理」算法實現,收集結束後能夠避免內存碎片問題。

G1優勢:

並行與併發:充分利用多CPU來縮短中止世界的停頓時間; 
2,分代收集:不須要其餘收集配合就能夠管理整個的Java堆,採用不一樣的方式處理新建的對象,已經存活一段時間和經歷過屢次GC的對象獲取更好的收集效果; 
3,空間整合:與CMS的」標記-清除」算法不一樣,G1在運行期間不會產生內存空間碎片,有利於應用的長時間運行,且分配大對象時,不會致使因爲沒法申請到足夠大的連續內存而提早觸發一次Full GC; 
4,停頓預測:G1中能夠創建可預測的停頓時間模型,能讓使用者明確指定在中號毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過Ñ毫秒。

使用G1收集器時,Java的堆的內存佈局與其餘收集器有很大區別,整個Java的堆會被劃分爲多個大小相等的獨立區域地區,新生代和老年代再也不是物理隔離了,都是一部分區域(不須要連續)的集合.G1會跟蹤各個地區的垃圾收集狀況(回收空間大小和回收消耗的時間),維護一個優先列表,根據容許的收集時間,優先回收價值最大的區域中,避免在整個的Java堆上進行全區域的垃圾回收,確保了G1收集器能夠在有限的時間內儘量收集更多的垃圾。

不過問題來了:使用G1收集器,一個對象分配在某個區域中,能夠和Java的堆上任意的對象有引用關係,那麼如何斷定一個對象是否存活,是否須要掃描整個Java的堆其實這個問題在以前收集器中也存在,若是回收新生代的對象時,不得不不一樣時掃描老年代的話,會大大下降Minor GC的效率。

針對這種狀況,虛擬機提供了一個解決方案:G1收集器中區域之間的對象引用關係和其餘收集器中新生代與老年代之間的對象引用關係被保存在Remenbered Set數據結構中,用來避免全堆掃描.G1中每一個區域都有一個對應的Remenbered Set,當虛擬機發現程序對參考類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查參考引用的對象是否處於相同的區域中,若是不是,則經過CardTable把相關引用信息記錄到被引用對象所屬區域的Remenbered Set中。
在互聯網公司面試中,架構的底層必定是面試官會問問的問題,針對面試官通常會提到的問題,我錄製了一些分佈式,微服務,性能優化等技術點底層原理的錄像視頻,加羣619881427能夠免費獲取這些錄像,裏面還有些分佈式,微服務,性能優化,春天設計時,MyBatis的等源碼知識點的錄像視頻。這些視頻都是 找一些資深架構師朋友一塊兒錄製出來的,這些視頻幫助如下幾類程序員:

1.對如今的薪資不滿,想要跳槽,卻對本身的技術沒有信心,不知道如何面對面試官。

2.想從傳統行業轉行到互聯網行業,但沒有接觸過互聯網技術。

3.工做1 - 5年須要提高本身的核心競爭力,但學習沒有系統化,不知道本身接下來要學什麼纔是正確的,踩坑後又不知道找誰,百度後依然不知因此然。

4.工做5 - 10年沒法突破技術瓶頸(運用過不少技術,在公司一直寫着業務代碼,卻依然不懂底層實現原理)

若是你如今正處於我上述所說的幾個階段能夠加下個人羣來學習。並且我也可以提供一些面試指導,職業規劃等建議。

相關文章
相關標籤/搜索