深刻了解Java虛擬機(2)垃圾收集器與內存分配策略

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

   因爲JVM中對象的頻繁操做是在堆中,因此主要回收的是堆內存,方法區中的回收也有,可是比較謹慎html

1、對象死亡判斷方法

  1.引用計數法

    就是若是對象被引用一次,就給計數器+1,不然-1java

    實現簡單,可是沒法解決對象相互引用的問題;實際上JVM也不是使用的此種方式,所以已下的程序咱們會看到內存被回收了算法

/**
 *testGC()方法執行後,objA和objB會不會被GC呢?
 *@author zzm
 */
class ReferenceCountingGC{
    public Object instance=nullprivate static final int _1MB = 1024*1024;
    /**
     *這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過
     */
    private byte[]bigSize=new byte[2*_1MB];
    public static void testGC(){
        ReferenceCountingGC objA=new ReferenceCountingGC();
        ReferenceCountingGC objB=new ReferenceCountingGC();
        objA.instance=objB;
        objB.instance=objA;
        objA=null;
        objB=null;
        //假設在這行發生GC,objA和objB是否能被回收?
        System.gc();
    }
}

  2.可達性分析

    定義一些GCroot,若是從GCroot到對象是不可達的,那麼對象就能夠被回收安全

    可能的gcroot:棧中的存放的對象的引用、方法區中靜態屬性和常量引用的對象、本地方法棧引用的對象(native)數據結構

    主流的jvm都是使用的此種方式多線程

    

 

  3.引用

    不管經過什麼方式,都是經過「引用」來判斷!併發

    在JVM中引用分爲四種:強、軟、弱、虛(具體參考這篇文章:http://www.cnblogs.com/zhangxinly/p/6978355.html框架

  4.finalize方法

    若是對象不可達,對象將被標記,jvm

    類覆寫了此方法,且對象的此方法從未被JVM執行過,則對象被放入一個隊列,等待一個線程來執行此對象的方法(注意只會執行一次)oop

    可使用這種特性在對象不可達,被發現爲可回收的狀態下,從新回收對象;就是在finalize方法中從新創建強引用

    不建議使用,瞭解便可 

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

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

    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:(");
        }
    }
}

  5.方法區回收

    方法區中的常量回收:在程序中沒有任何地方使用到如:str=「abc」,則回收

    類回收:類全部實例全被回收、類加載器被回收、類的Class對象未被引用沒法在任何地方經過反射調用,則該類能夠被回收卸載

    在現在框架動態代理大行其道的今天,JVM必須有卸載類的方法,否則出現泄漏

2、回收算法

  1.標記-清除算法

    標記不可達對象,而後jvm進行統一回收

    缺點:

      效率不高,兩個過程效率都不高

      回收後內存不連續,由於是從中移除掉不可達的,會致使大量碎片,若是JVM要分配一個連續的大內存,將會產生問題

  2.複製算法

    1)將內存分爲兩份A和B,若是A不夠用了,就將A中存活的對象複製到B中(複製過去的確定小於等於原來的)

    2)而後將A清空,等待B滿了以後再次執行相反的動做;循環往復

    問題:內存只能使用一半

    優勢:迅速,複製以後內存空間連續

    使用:在新生代中,對象的建立和死亡是十分快的,這就保證了每次從A複製到B中的會不多(大量的被回收),因此A和B不須要同樣大,甚至B能夠很小

    在主流虛擬機中使用的是這種方法,分爲A/B/C三快,比例爲8:1:1,將A和B複製到C

   3.標記整理

    也是將須要回收的標記

    而後不統一回收,而是將存活的統一移動到一端

    最後將端外的所有回收

   4.分代算法

    分代:根據對象的存活週期將內存分代如:新生代(對象建立死亡活躍)和老年代(比較穩定)

    根據以上的介紹:在新生代中就適合用複製算法,在老年代中就適合用標記整理/清理算法

    就是複製+標記整理兩種算法結合

3、HotSpot的算法實現

  1.枚舉根節點

    根節點不少,若是要逐個檢查這裏面的引用會浪費時間

    GC停頓,爲了在gc時引用狀態不改變,須要停頓全部執行線程,直至gc完成

    因此:JVM有方法直接知道哪些地方存放這引用;經過oopMap這樣的數據結構來實現

    oop:     

      在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。

      這樣,GC在掃描時就能夠直接得知這些信息了

  2.安全點  

    在特定的位置記錄信息,進行GC;此時須要讓線程都跑到安全點掛起

    這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)

    其中搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。

    而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌

    發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的

  3.安全區域

    若是程序沒有執行:沒有CUP時間片(sleep、blocked等),線程是沒法響應中斷的,也就無法去安全點進行掛起

    解決:使用安全區域,在safe region中任意地方開始GC都是安全的,就不須要線程跑到安全點了

    流程:

      當線程進入safe region時標識本身進入safe region;

      當GC時無論safe region狀態的線程;

      當線程要出來時,要判斷系統是否已經枚舉GCroot完成,不然要等待其完成才能出safe region

4、垃圾收集器

    先來一張圖,瞭解HotSpot中的垃圾收集器;

    

 

  1.Serial收集器(單線程,新生代)

    特色:

      這是一個單線程收集器

      收集時,中止全部工做線程,直到它工做結束

      會致使程序停頓

    場景:

      在單cpu環境中,簡單高效,沒有線程交互開銷,專心作垃圾收集

      在桌面客戶端client應用中,交給JVM的內存管理不會太多,使用也不會形成長時間停頓

   2.ParNew收集器(多線程,老年代)

    能夠認爲是Serial的多線程版本;

    特色:

      多線程並行收集;可是用戶線程仍是所有暫停

      在多cup中有優點,在單核系統中不必定比serial好,由於存在線程切換開銷

    場景:

      因爲HotSpot推出了劃時代的CMS收集器做爲老年代的收集器,卻只有ParNew能與之共同工做來收集新生代

 

  3.Paraller Scavenge 收集器(吞吐量收集器,gc自適應調節)

    新生代收集器,也是並行採用複製算法,可是能夠手動或自動調節cpu的吞吐量,  

      所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

      GC停頓時間短適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗;高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

    -XX:MaxGCPauseMillis參數:控制最大垃圾收集停頓時間的

      MaxGCPauseMillis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超過設定值。不過你們不要認爲若是把這個參數的值設置得稍小一點就能使

      得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代確定比收集500MB快吧,這也直接致使垃圾

      收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,如今變成5秒收集一次、每

      次停頓70毫秒。停頓時間的確在降低,但吞吐量也降下來了。

    -XX:GCTimeRatio參數:直接設置吞吐量大小的

      GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,至關因而吞吐量的倒數。

      若是把此參數設置爲19,那容許的最大GC時間就佔總時間的5%(即1/(1+19)),默認值爲99,就是容許最大1%(即1/(1+99))的垃圾收集時間。

    +UseAdaptiveSizePolicy:這是一個開關參數

      當這個參數打開以後,就不須要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數

      虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)[1]。

      只須要把基本的內存數據設置好(如-Xmx設置最大堆),而後使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標

      自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別

  4.Serial Old

    顧名思義,Serial的老年代版本

    做爲CMS收集器的後備預案,在併發收集Concurrent Mode Failure時使用

    

  5.Parallel old收集器

    顧名思義,Parallel的老年代版本

    這樣就組成了:完整的新生代和老年代吞吐量收集器

    

 

   6.CMS收集器(可併發)

    階段:  

      初始標記(CMS initial mark):標記GC Roots能直接關聯到的對象,速度很快;

      併發標記(CMS concurrent mark):gc可達性GC RootsTracing的過程

      從新標記(CMS remark):修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄

      併發清除(CMS concurrent sweep):清除標記的內存

    詳解:  

      初始標記、從新標記這兩個步驟仍然須要「Stop The World」。

      而從新標記階段這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。

      因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

     缺點:

      對CPU資源敏感:啓動的線程=(cpu+3)/4,也就是cpu多則佔用整個系統的資源少,反之則相反;在cpu少的狀況下會使系統忽然變慢

      浮動垃圾,因爲在清除時,用戶線程還在運行產生垃圾,這些垃圾只能等下次GC

      基於標記-清除,產生大量碎片;

    

 

  7.G1收集器(最新)

    可預測的停頓;標記整理+複製,無CMS的碎片問題

    分析:

      在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代;

      G1收集器它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。

      G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。

      G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region

      這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。

      G1把內存「化整爲零」的:

      把Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?

      Region不多是孤立的。一個對象分配在某個Region中,它並不是只能被本Region中的其餘對象引用,而是能夠與整個Java堆任意的對象發生引用關係。

      那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準確性?

      這個問題其實並不是在G1中才有,只是在G1中更加突出而已。

      在之前的分代收集中,新生代的規模通常都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象時也面臨相同的問題,若是回收新生代時也不得不一樣時掃描老年代的話,那麼Minor GC的效率可能降低很多。

      在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。

      G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。

      當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。

    過程:與CMS很相似

      初始標記(Initial Marking)

      併發標記(Concurrent Marking)

      最終標記(Final Marking)

      篩選回收(Live Data Counting and Evacuation)

    

 5、內存分配與回收策略  

  1.對象優先在Eden分配

  2.大對象直接分配在老年代上:其閥值控制:-XX:PretenureSizeThreshould=字節

  3.長期存貨的對象進入老年代:其閥值(每通過一次複製,值+1):-XX:MaxTenuringThreshold=15

  4.動態對象年齡判斷:

    不必定必定要達到閥值才放入老年代:當Survivor中相同年齡的對象>=Survivor的一半時,這些對象直接進入老年代

  5.空間分配擔保

    當Eden存活對象複製入Survivor中,若是空間不夠,複製進入老年代中,在複製進老年代時,也要判斷空間大小(值爲歷次進入老年代對象的平均值)

 

附錄:垃圾收集相關常數

  

  

相關文章
相關標籤/搜索