深入瞭解Java虛擬機(2)垃圾收集器與記憶體分配策略

来源:http://www.cnblogs.com/zhangxinly/archive/2017/08/02/6964861.html
-Advertisement-
Play Games

垃圾收集器與記憶體分配策略 由於JVM中對象的頻繁操作是在堆中,所以主要回收的是堆記憶體,方法區中的回收也有,但是比較謹慎 一、對象死亡判斷方法 1.引用計數法 就是如果對象被引用一次,就給計數器+1,否則-1 實現簡單,但是無法解決對象相互引用的問題;實際上JVM也不是使用的此種方式,因此已下的程式我 ...


垃圾收集器與記憶體分配策略

   由於JVM中對象的頻繁操作是在堆中,所以主要回收的是堆記憶體,方法區中的回收也有,但是比較謹慎

一、對象死亡判斷方法

  1.引用計數法

    就是如果對象被引用一次,就給計數器+1,否則-1

    實現簡單,但是無法解決對象相互引用的問題;實際上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執行過,則對象被放入一個隊列,等待一個線程來執行此對象的方法(註意只會執行一次)

    可以使用這種特性在對象不可達,被髮現為可回收的狀態下,重新回收對象;就是在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必須有卸載類的方法,不然出現泄漏

二、回收演算法

  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.分代演算法

    分代:根據對象的存活周期將記憶體分代如:新生代(對象創建死亡活躍)和老年代(比較穩定)

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

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

三、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

四、垃圾收集器

    先來一張圖,瞭解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)

    

 五、記憶體分配與回收策略  

  1.對象優先在Eden分配

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

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

  4.動態對象年齡判斷:

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

  5.空間分配擔保

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

 

附錄:垃圾收集相關常數

  

  

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Description: 我們現在要利用 m 台機器加工 n 個工件,每個工件都有 m 道工序,每道工序都在不同的指定的機器上完成。每個工件的每道工序都有指定的加工時間。 每個工件的每個工序稱為一個操作,我們用記號 j-k 表示一個操作,其中 j 為 1 到 n 中的某個數字,為工件號; k 為 1 ...
  • Java的MVC模式簡介 MVC(Model View Control)模型-視圖-控制器 首先我們需要知道MVC模式並不是javaweb項目中獨有的,MVC是一種軟體工程中的一種軟體架構模式,把軟體系統分為三個基本部分:模型(Model)、視圖(View)和控制器(Controller),即為MV ...
  • 搶購、秒殺是平常很常見的場景,面試的時候面試官也經常會問到,比如問你淘寶中的搶購秒殺是怎麼實現的等等。 搶購、秒殺實現很簡單,但是有些問題需要解決,主要針對兩個問題: 1 高併發對資料庫產生的壓力 2 競爭狀態下如何解決庫存的正確減少("超賣"問題) 第一個問題,對於PHP來說很簡單,用緩存技術就可 ...
  • 我們現在介面的線上問題主要有三個,第一:啟動時有些機器會有短暫的線程池滿。第二:併發量上不去,怕服務被打死,不敢調高限流閾值。第三:499超時現象。 今天已上線 今天終於把那天說的全量執行時間延長,從圖中可以看到,中午12點發版之後,記憶體使用率有明顯下降,晚上是介面調用高峰,會有上浮,但是總體來看還 ...
  • 1、下載solr6.6 並解壓 地址: http://www.apache.org/dyn/closer.lua/lucene/solr/6.6.0 2、安裝JDK1.8 地址: http://www.oracle.com/technetwork/java/javase/downloads/jdk8 ...
  • 1 //選擇排序對數據進行升序排序 2 public static void selectSortArray(int[] arr){ 3 for(int i = 0; iarr[j]){ 6 int temp = arr[j]; 7 arr[j... ...
  • 裝箱和拆箱 裝箱和拆箱 基本數據類型的包裝類 舉兩個例子,看一下 基本數據類型的包裝類 舉兩個例子,看一下 對於byte/short/long/float/double和Integer(int)類用法類似 對於byte/short/long/float/double和Integer(int)類用法類 ...
  • 有時候,需要禁止函數修改列表。例如要對裂變進行修改操作,也要保留原來的未列印的設計列表,以供備案。為解決這個問題,可向函數傳遞列表的副本而不是原件;這樣函數所做的任何修改都隻影響副本,而絲毫不影響原件。 8-9 魔術師 魔術師 :創建一個包含魔術師名字的列表,並將其傳遞給一個名為show_magic ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...