主頁 > 後端開發 > java垃圾收集器與記憶體分配策略(2)

java垃圾收集器與記憶體分配策略(2)

2022-08-18 10:11:00 後端開發

1.概述

??說起垃圾收集(Garbage Collection, GC),大部分人都把這項技術當做Java語言的伴生產物,事實上,GC的歷史遠遠比Java久遠,1960年誕生于MIT的Lisp是第一門 真正使用記憶體動態分配和垃圾收集技術的語言,當Lisp還在胚胎時期時,人們就在思考 GC需要完成的三件事情:

  • 哪些記憶體需要回收?
  • 什么時候回收?
  • 如何回收?

??經過半個世紀的發展,記憶體的動態分配與記憶體回收技術已經相當成熟,一切看起來 都進入了 '‘自動化”時代,那為什么我們還要去了解GC和記憶體分配呢?答案很簡單: 當需要排査各種記憶體溢位、記憶體泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶 頸時,我們就需要對這些'‘自動化”的技術實施必要的監控和調節,

??把時間從半個世紀以前撥回到現在,回到我們熟悉的Java語言,第2章介紹了 Java 記憶體運行時區域的各個部分,其中程式計數器、虛擬機堆疊、本地方法堆疊三個區域隨執行緒而生,隨執行緒而滅;堆疊中的堆疊幀隨著方法的進入和退出而有條不紊地執行著出堆疊和入堆疊 操作,每一個堆疊幀中分配多少記憶體基本上是在類結構確定下來時就已知的(盡管在運行期會由JIT編譯器進行一些優化,但在本章基于概念模型的討論中,大體上可以認為是 編譯期可知的),因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內不需要過多考慮回收的問題,因為方法結束或執行緒結束時,記憶體自然就跟隨著回收了,而 Java堆和方法區則不一樣,一個介面中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處于運行期間時才能知道會創建哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的是這部分記憶體,后續討論中的"記憶體”分配與回收也僅指這一部分記憶體,

2. 物件巳死?

??堆中幾乎存放著Java世界中所有的物件實體,垃圾收集器在對堆進行回收前,第一 件事情就是要確定這些物件有哪些還"存活”著,哪些已經“死去”(即不可能再被任何途徑使用的物件),

??2.1 參考計數演算法

??很多教科書判斷物件是否存活的演算法是這樣的:給物件中添加一個參考計數器,每當有一個地方參考它時,計數器值就加1 ;當參考失效時,計數器值就減1 ;任何時刻計數器都為0的物件就是不可能再被使用的,筆者面試過很多的應屆生和一些有多年工 作經驗的開發人員,他們對于這個問題給予的都是這個答案,

??客觀地說,參考計數演算法(Reference Counting)的實作簡單,判定效率也很高, 在大部分情況下它都是一個不錯的演算法,也有一些比較著名的應用案例,例如微軟的 COM (Component Object Model)技術、使用 ActionScript 3 的 FlashPlayer、Python 語 言以及在游戲腳本領域中被廣泛應用的Squirrel中都使用了參考計數演算法進行記憶體管理, 但是,Java語言中沒有選用參考計數演算法來管理記憶體,其中最主要的原因是它很難解決 物件之間的相互回圈參考的問題,

??舉個簡單的例子,請看代碼清單3-1中的testGCO方法:物件objA和objB都有欄位 instance,賦值令 objA.instance = objB 及 objB.instance = objA,除此之外,這兩個物件再無任何參考,實際上這兩個物件已經不可能再被訪問,但是它們因為互相參考著對方,導致它們的參考計數都不為0,于是參考計數演算法無法通知GC收集器回收它們,

package com.intehel.demo.domain;

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    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;
        System.gc();
    }
}

??程式正常退出,虛擬機并沒有因為這兩個物件互相參考就不回收它們,這也從側面說明虛擬機并不是通過參考計數演算法來判斷物件是否存活的,

??2.2 根搜索演算法

??在主流的商用程式語言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索演算法(GC Roots Tracing)判定物件是否存活的,這個演算法的基本思路就是查 過一系列的名為“GC Roots”的物件作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為參考鏈Reference Chain),當一個物件到GC Roots沒有任何參考鏈相連(用圖論的話來說就是從GC Roots到這個物件不可達)時,則證明此物件是不可用 的,如圖3-1所示,物件,bject5、object 6. object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件,

??在Java語言里,可作為GC Roots的物件包括下面幾種:

  • 虛擬機堆疊(堆疊幀中的本地變數表)中的參考的物件,
  • 方法區中的類靜態屬性參考的物件,
  • 方法區中的常量參考的物件,
  • 本地方法堆疊中JNI (即一般說的Native方法)的參考的物件,

??2.3 再談參考

??無論是通過參考計數演算法判斷物件的參考數量,還是通過根搜索演算法判斷物件的引 用鏈是否可達,判定物件是否存活都與“參考”有關,JDK 1.2之前,Java中的參考 的定義很傳統:如果reference型別的資料中存盤的數值代表的是另外一塊記憶體的起始 地址,就稱這塊記憶體代表著一個參考,這種定義很純粹,但是太過狹隘,一個物件在這 種定義下只有被參考或者沒有被參考兩種狀態,對于如何描述一些“食之無味,棄之可惜”的物件就顯得無能為力,我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體在進行垃圾收集后還是非常緊張,則可以拋棄這些物件, 很多系統的快取功能都符合這樣的應用場景,

??在JDK 1.2之后,Java對參考的概念進行了擴充,將參考分為強參考(Strong Reference),軟參考(Soft Reference)、弱參考(Weak Reference)、虛參考(Phantom Reference)四種,這四種參考強度依次逐漸減弱,

  • 強參考就是指在程式代碼之中普遍存在的,類似“Object obj = new Object()”這類的參考,只要強參考還存在,垃圾收集器永遠不會回收掉被參考的物件,
  • 軟參考用來描述一些還有用,但并非必需的物件,對于軟參考關聯著的物件,在 系統將要發生記憶體溢位例外之前,將會把這些物件列進回收范圍之中并進行第二 次回收,如果這次回識訓是沒有足夠的記憶體,才會拋出記憶體溢位例外,在JDK 1.2之后,提供了 SoftReference類來實作軟參考,
  • 弱參考也是用來描述非必需物件的,但是它的強度比軟參考更弱一些,被弱參考 關聯的物件只能生存到下一次垃圾收集發生之前,當垃圾收集器作業時,無論當 前記憶體是否足夠,都會回收掉只被弱參考關聯的物件,在JDK 1.2之后,提供了 WeakReference類來實作弱參考,
  • 虛參考也稱為幽靈參考或者幻影參考,它是最弱的一種參考關系,一個物件是否 有虛參考的存在,完全不會對其生存時間構成影響,也無法通過虛參考來取得一個物件實體,為一個物件設定虛參考關聯的唯一目的就是希望能在這個物件被收 集器回收時收到一個系統通知,在JDK 1.2之后,提供了 PhantomReference類來 實作虛參考,

??2.4 生存還是死亡?

??在根捜索演算法中不可達的物件,也并非是“非死不可”的,這時候它們暫時處于 “緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記程序:如果物件在進行 根捜索后發現沒有與GC Roots相連接的參考鏈,那它將會被第一次標記并且進行一次 篩選,篩選的條件是此物件是否有必要執行finalize()方法,當物件沒有覆寫finalize()方法,或者finalize()方法已經被虛擬機呼叫過,虛擬機將這兩種情況都視為'‘沒有必要 執行”,

??如果這個物件被判定為有必要執行finalize()方法,那么這個物件將會被放置在一個 名為F-Queue的佇列之中,并在稍后由一條由虛擬機自動建立的、低優先級的Finalizer 執行緒去執行,這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死回圈(更極端的情況),將很可能會導致F-Queue佇列中的其他物件永久處于等待狀態, 甚至導致整個記憶體回收系統崩潰,finalize()方法是物件逃脫死亡命運的最后一次機會, 稍后GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與參考鏈上的任何一個物件建立關聯即可,譬如把自己(this 關鍵字)賦值給某個類變數或物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那它就真的離死不遠了,從代碼清單3-2 中我們可以看到一個物件的finalize()被執行,但是它仍然可以存活,

package com.intehel.demo.domain;

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive() {
        System.out.println("I am still alive");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Finalized method execution");
        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("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("I am dead!");
        }
    }
}

??運行結果:

??Finalized method execution
??I am still alive
??I am dead!

??從代碼清單3-2的運行結果可以看到,SAVE_HOOK物件的finalize()方法確實被 GC收集器觸發過,并且在被收集前成功逃脫了,

??另外一個值得注意的地方就是,代碼中有兩段完全一樣的代碼片段,執行結果卻是次逃脫成功,一次失敗,這是因為任何一個物件的finalize,方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了,

??需要特別說明的是,上面關于物件死亡時finalize()方法的描述可能帶有悲情的藝術色彩,筆者并不鼓勵大家使用這種方法來拯救物件,相反,筆者建議大家盡量避免使用 它,因為它不是C/C++中的解構式,而是Java剛誕生時為了使C/C++程式員更容易接受它所做出的一個妥協,它的運行代價高昂,不確定性大,無法保證各個物件的呼叫順序,有些教材中提到它適合做“關閉外部資源”之類的作業,這完全是對這種方法的用途的一種自我安慰,finalize,能做的所有作業,使用try-finally或其他方式都可以做得更好、更及時,大家完全可以忘掉Java語言中還有這個方法的存在,

??2.5 回收方法區

??很多人認為方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規范中確實說過可以不要求虛擬機在方法區實作垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收 集一般可以回收70%?95%的空間,而永久代的垃圾收集效率遠低于此,

??永久代的垃圾收集主要回收兩部分內容:廢棄常量無用的類,回收廢棄常量與回收Java堆中的物件非常類似,以常量池中字面量的回收為例,假如一個字串 “abc”已經進入了常量池中,但是當前系統沒有任何一個String物件是叫做“abc” 的,換句話說是沒有任何String物件參考常量池中的“abc”常量,也沒有其他地方參考了這個字面量,如果在這時候發生記憶體回收,而且必要的話,這個“abc”常量 就會被系統“請”出常量池,常量池中的其他類(介面)、方法、欄位的符號參考也 與此類似,

??判定一個常量是否是"廢棄常量”比較簡單,而要判定一個類是否是"無用的類" 的條件則相對苛刻許多,類需要同時滿足下面3個條件才能算是“無用的類”:

  • 該類所有的實體都已經被回收,也就是Java堆中不存在該類的任何實體,
  • 加載該類的ClassLoader已經被回收,
  • 該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,

??虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而不是和物件一樣,不使用了就必然會回收,是否對類進行回收,HotSpot虛擬機提供 了 -Xnoclassgc 引數進行控制,還可以使用-verbose:class 及-XX: +TraceClassLoading、 -XX:+TraceClassUnLoading 査看類的加載和卸載資訊,-verbose: class 和-XX: +TraceClassLoading 可以在 Product 版的虛擬機中使用,但-XX:+TraceClassUnLoading 引數需要fastdebug版的虛擬機支持,

??在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和 OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢位,

3. 垃圾收集演算法

??由于垃圾收集演算法的實作涉及大量的程式細節,而且各個平臺的虛擬機操作記憶體的 方法又各不相同,因此本節不打算過多地討論演算法的實作,只是介紹幾種演算法的思想及 其發展程序,

??3.1 標記-清除演算法

??最基礎的收集演算法是“標記-清除”(Mark-Sweep)演算法,如它的名字一樣,演算法分 “標記“和“清除“兩個階段:首先標記出所有需要回收的物件,在標記完成后統一回 收掉所有被標記的物件,它的標記程序其實在前一節講述物件標記判定時已經基本介紹過 了,之所以說它是最基礎的收集演算法,是因為后續的收集演算法都是基于這種思路并對其缺 點進行改進而得到的,它的主要缺點有兩個:一個是效率問題,標記和清除程序的效率都 不高:另外一個是空間問題,標記清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以后的運行程序中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,標記-清除演算法的執行程序如圖3-2所示,

??

??3.2 復制演算法

??為了解決效率問題,一種稱為"復制”(Copying)的收集演算法出現了,它將可用記憶體 按容量劃分為大小相等的兩塊,毎次只使用其中的一塊當這一塊的記憶體用完了,就將還 存活著的物件復制到另外一塊上面,然后再把已使用過的記憶體空間一次清理掉,這樣使得 每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等復雜情況,只 要移動堆頂指標,按順序分配記憶體即可,實作簡單,運行高效,只是這種演算法的代價是將記憶體縮小為原來的一半,未免太高了一點,復制演算法的執行程序如圖3-3所示,

??

??現在的商業虛擬機都采用這種收集演算法來回收新生代,IBM的專門研究表明,新生代中的物件98%是朝生夕死的,所以并不需要按照1 : 1的比例來劃分記憶體空間,而是 將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的 一塊Survivor"當回收時,將Eden和Survivor中還存活著的物件一次性地拷貝到另外 一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor的空間,HotSpot虛擬 機默認Eden和Survivor的大小比例是8 : 1,也就是每次新生代中可用記憶體空間為整個 新生代容量的90% (80%+10%),只有10%的記憶體是會被“浪費”的,當然,98%的 物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多于10%的對 象存活,當Survivor空間不夠用時,需要依賴其他記憶體(這里指老年代)進行分配擔保 (Handle Promotion),

??記憶體的分配擔保就好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能 按時償還,于是銀行可能會默認我們下一次也能按時按量地償還貸款,只需要有一個擔 保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了,記憶體的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集 下來的存活物件,這些物件將直接通過分配擔保機制進入老年代,關于對新生代進行分 配擔保的內容,本章稍后在講解垃圾收集器執行規則時還會再詳細講解,

??3.3 標記-整理演算法

??復制收集演算法在物件存活率較高時就要執行較多的復制操作,效率將會變低,更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法,

??根據老年代的特點,有人提出了另外一種“標記-整理"(Mark-Compact)演算法, 標記程序仍然與“標記-清除”演算法一樣,但后續步驟不是直接對可回收物件進行清 理,而是讓所有存活的物件都向一端移動,然后直接清理掉端邊界以外的記憶體,“標記-整理”演算法的示意圖如圖3-4所示,

??

??3.4 分代收集演算法

??當前商業虛擬機的垃圾收集都采用“分代收集”(Generational Collection)演算法,這 種演算法并沒有什么新的思想,只是根據物件的存活周期的不同將記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算 法,在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用復 制演算法,只需要付出少量存活物件的復制成本就可以完成收集,而老年代中因為物件存 活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整 理”演算法來進行回收,

4.垃圾收集器

??如果說收集演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實作,Java 虛擬機規范中對垃圾收集器應該如何實作并沒有任何規定,因此不同的廠商、不同版本 的虛擬機所提供的垃圾收集器都可能會有很大的差別,并且一般都會提供引數供用戶根 據自己的應用特點和要求組合出各個年代所使用的收集器,這里討論的收集器基于Sun HotSpot虛擬機1.6版Update 22,這個虛擬機包含的所有收集器如圖3-5所示,

??

??圖3-5展示了 7種作用于不同分代的收集器(包括JDK 1.6_Update14后引入的 Early Access版G1收集器),如果兩個收集器之間存在連線,就說明它們可以搭配使用,

??在介紹這些收集器各自的特性之前,我們先來明確一個觀點:雖然我們是在對各個 收集器進行比較,但并非為了挑選一個最好的收集器出來,因為直到現在為止還沒有最 好的收集器出現,更加沒有萬能的收集器,所以我們選擇的只是對具體應用最合適的收 集器,這點不需要多加解釋就能證明:如果有一種放之四海皆準、任何場景下都適用的 完美收集器存在,那HotSpot虛擬機就沒必要實作那么多不同的收集器了,

??4.1 Serial 收集器

??Serial收集器是最基本、歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機 新生代收集的唯一選擇,大家看名字就知道,這個收集器是一個單執行緒的收集器,但它 的'‘單執行緒”的意義并不僅僅是說明它只會使用一個CPU或一條收集執行緒去完成垃圾收 集作業,更重要的是在它進行垃圾收集時,必須暫停其他所有的作業執行緒(Sun將這件 事情稱之為“Stop The World"),直到它收集結束,“Stop The World"這個名字也許聽 起來很酷,但這項作業實際上是由虛擬機在后臺自動發起和自動完成的,在用戶不可見 的情況下把用戶的正常作業的執行緒全部停掉,這對很多應用來說都是難以接受的,你想 想,要是你的電腦每運行一個小時就會暫停回應5分鐘,你會有什么樣的心情?圖3-6 示意了 Serial / Serial Old收集器的運行程序,

??對于“Stop The World”帶給用戶的惡劣體驗,虛擬機的設計者們表示完全理解,但也表示非常委屈:“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實地在椅子上 或房間外待著,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完嗎? ”這確實是 一個合情合理的矛盾,雖然垃圾收集這項作業聽起來和打掃房間屬于一個性質的,但實際上肯定還要比打掃房間復雜得多啊!

??

??

??從JDK 1.3開始,一直到現在還沒正式發布的JDK 1.7,HotSpot虛擬機開發團隊 為消除或減少作業執行緒因記憶體回收而導致停頓的努力一直在進行著,從Serial收集器 到Parallel收集器,再到Concurrent Mark Sweep (CMS)現在還未正式發布的Garbage First (G1)收集器,我們看到了一個個越來越優秀(也越來越復雜)的收集器的出現, 用戶執行緒的停頓時間在不斷縮短,但是仍然沒有辦法完全消除(這里暫不包括RTSJ中 的收集器),尋找更優秀的垃圾收集器的作業仍在繼續!

??寫到這里,筆者似乎已經把Serial收集器描述成一個老而無用,食之無味棄之可惜 的雞肋了,但實際上到現在為止,它依然是虛擬機運行在Client模式下的默認新生代收 集器,它也有著優于其他收集器的地方:簡單而高效(與其他收集器的單執行緒比),對 于限定單個CPU的環境來說,Serial收集器由于沒有執行緒互動的開銷,專心做垃圾收 集自然可以獲得最高的單執行緒收集效率,在用戶的桌面應用場景中,分配給虛擬機管理 的記憶體一般來說不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的 記憶體,桌面應用基本上不會再大了),停頓時間完全可以控制在幾十毫秒最多一百多毫 秒以內,只要不是頻繁發生,這點停頓是可以接受的,所以,Serial收集器對于運行在 Client模式下的虛擬機來說是一個很好的選擇,

??4.2 ParNew 收集器

??ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾 收集之外,其余行為包括Serial收集器可用的所有控制引數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集演算法、Stop The World,物件分配規則、回收策略等都與Serial收集器完全一樣,實作上這兩種收集器也 共用了相當多的代碼,ParNew收集器的作業程序如圖3-7所示,

??

 

??ParNew收集器除了多執行緒收集之外,其他與Serial收集器相比并沒有太多創新之 處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與 性能無關但很重要的原因是,除了 Serial收集器外,目前只有它能與CMS收集器配合作業,在JDK 1.5時期,HotSpot推出了一款在強互動應用中幾乎可稱為有劃時代意義 的垃圾收集器——CMS收集器(Concurrent Mark Sweep,本節稍后將詳細介紹這款收集 器),這款收集器是HotSpot虛擬機中第一款真正意義上的并發(Concurrent)收集器, 它第一次實作了讓垃圾收集執行緒與用戶執行緒(基本上)同時作業,用前面那個例子的話 來說,就是做到了在你媽媽打掃房間的時候你還能同時往地上扔紙屑,

??不幸的是,它作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收 集器Parallel Scavenge配合作業,所以在JDK 1.5中使用CMS來收集老年代的時 候,新生代只能選擇ParNew或Serial收集器中的一個,ParNew收集器也是使用-XX: WseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC 選項來強制指定它,

??ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由于存在執行緒互動的開銷,該收集器在通過超執行緒技術實作的兩個CPU的環境中都不能百分之 百地保證能超越Serial收集器,當然,隨著可以使用的CPU的數量的增加,它對于GC時 系統資源的利用還是很有好處的,它默認開啟的收集執行緒數與CPU的數量相同,在CPU 非常多(譬如32個,現在CPU動輒就4核加超執行緒,服務器超過32個邏輯CPU的情況 越來越多了)的環境下,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數,

??注意從ParNew收集器開始,后面還將會接觸到幾款并發和并行的收集器,在大家可能 產生疑惑之前,有必要先解釋兩個名詞:并發和并行,這兩個名詞都是并發編程中的概 念,在談論垃圾收集器的背景關系語境中,他們可以解釋為:

  • 并行(Parallel):指多條垃圾收集執行緒并行作業,但此時用戶執行緒仍然處于等待狀態,
  • 并發(Concurrent):指用戶執行緒與垃圾收集執行緒同時執行(但不一定是并行的,可能會交替執行),用戶程式繼續運行,而垃圾收集程式運行于另一個CPU上,

??4.3 Parallel Scavenge 收集器

??Parallel Scavenge收集器也是一個新生代收集器,它也是使用復制演算法的收集器, 又是并行的多執行緒收集器……看上去和ParNew都一樣,那它有什么特別之處呢?

??Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的 關注點盡可能地縮短垃圾收集時用戶執行緒的停頓時間,而Parallel Scavenge收集器的目 標則是達到一個可控制的吞吐量(Throughput)所謂吞吐量就是CPU用于運行用戶代 碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/ (運行用戶代碼時 間+垃圾收集時間),虛擬機總共運行了 100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%,

??停頓時間越短就越適合需要與用戶互動的程式,良好的回應速度能提升用戶的體 驗;而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程式的運算任務,主要適 合在后臺運算而不需要太多互動的任務

??Parallel Scavenge收集器提供了兩個引數用于精確控制吞吐量,分別是控制 最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數及直接設定吞吐量大小的 -XX:GCTimeRatio 引數,

??MaxGCPauseMillis引數允許的值是一個大于0的毫秒數,收集器將盡力保證記憶體回 識訓費的時間不超過設定值,不過大家不要異想天開地認為如果把這個引數的值設定得 稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和 新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB 快吧,這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100 毫秒,現在變成5秒收集一次、每次停頓70毫秒,停頓時間的確在下降,但吞吐量也 降下來了,

??GCTimeRatio引數的值應當是一個大于0小于100的整數,也就是垃圾收集時間占 總時間的比率,相當于是吞吐量的倒數,如果把此引數設定為19,那允許的最大GC時 間就占總時間的5% (即1/ (1 + 19)),默認值為99,就是允許最大1% (即1 / (1+99)) 的垃圾收集時間,

??由于與吞吐量關系密切,Parallel Scavenge收集器也經常被稱為“吞吐量 優先”收集器,除上述兩個引數之外,Parallel Scavenge收集器還有一個參
-XX+UseAdaptiveSizePolicy值得關注,這是一個開關引數,當這個引數打開之后,就 不需要手工指定新生代的大小(-Xmn), Eden與Survivor E的比例(-XX:SurvivorRatio)、 晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機會根據當 前系統的運行情況收集性能監控資訊,動態調整這些引數以提供最合適的停頓時間或 最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics) 如果 讀者對于收集器運作原理不太了解,手工優化存在困難的時候,使用Parallel Scavenge 收集器配合自適應調節策略,把記憶體管理的調優任務交給虛擬機去完成將是一個很 不錯的選擇,只需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然后使用 MaxGCPauseMillis引數(更關注最大停頓時間)或GCTimeRatio引數(更關注吞吐量) 給虛擬機設立一個優化目標,那具體細節引數的調節作業就由虛擬機完成了,自適應調 節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別,

??4.4 Serial Old 收集器

??Serial Old是Serial 收集器的老年代版本,它同樣是一個單執行緒收集器,使用 記-整理”演算法,這個收集器的主要意義也是被Client模式下的虛擬機使用,如果 在Server模式下,它主要還有兩大用途:一個是在JDK 1.5及之前的版本中與Parallel Scavenge收集器搭配使用氣 另外一個就是作為CMS收集器的后備預案,在并發收集發 生Concurrent Mode Failure的時候使用,這兩點都將在后面的內容中詳細講解,Serial Old收集器的作業程序如圖3-8所示,

??

??4.5 Parallel Old 收集器

??Parallel OldParallel Scavenge收集器的老年代版本,使用多執行緒和標記-整理” 演算法,這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge 收集器一直處于比較尷尬的狀態,原因是,如果新生代選擇了 Parallel Scavenge收集器, 老年代除了 Serial Old (PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器無法與CMS收集器配合作業嗎?),由于單執行緒的老年代Serial Old收 集器在服務端應用性能上的'‘拖累”,即便使用了 Parallel Scavenge收集器也未必能在整 體應用上獲得吞吐量最大化的效果,又因為老年代收集中無法充分利用服務器多CPU的 處理能力,在老年代很大而且硬體比較高級的環境中,這種組合的吞吐量甚至還不…定 有ParNew加CMS的組合“給力”,

??直到Parallel Old收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的應 用組合,在注重吞吐量及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加 Parallel Old收集器,Parallel Old收集器的作業程序如圖3-9所示,

??

??4.6 CMS收集器

??CMS (Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收 集器,目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用 尤其重視服務的回應速度,希望系統停頓時間最短,以給用戶帯來較好的體驗,CMS收 集器就非常符合這類應用的需求,

??從名字(包含“Mark Sweep")上就可以看出CMS收集器是基于“標記-清除”演算法實作的,它的運作程序相對于前面幾種收集器來說要更復雜一些,整個程序分為4個步驟,包括:

  • 初始標記(CMS initial mark)
  • 并發標記(CMS concurrent mark)
  • 重新標記(CMS remark)
  • 并發清除(CMS concurrent sweep)

??其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”,初始標記僅僅 只是標記一下GC Roots能直接關聯到的物件,速度很快,并發標記階段就是進行GC Roots Tracing的程序,而重新標記階段則是為了修正并發標記期間,因用戶程式繼續運 作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始 標記階段稍長一些,但遠比并發標記的時間短,

??由于整個程序中耗時最長的并發標記和并發清除程序中,收集器執行緒都可以與用戶執行緒一起作業,所以總體上來說,CMS收集器的記憶體回收程序是與用戶執行緒一起并發地執行的,通過圖3-10可以比較清楚地看到CMS收集器的運作步驟中并發和需要停頓的時間,

??

??CMS是一款優秀的收集器,它的最主要優點在名字上已經體現出來了 :并發收集、 低停頓,Sun的一些官方檔案里面也稱之為并發低停頓收集器(Concurrent Low Pause Collector),但是CMS還遠達不到完美的程度,它有以下三個顯著的缺點:

  •  CMS收集器對CPU資源非常敏感,其實,面向并發設計的程式都對CPU資源比 較敏感,在并發階段,它雖然不會導致用戶執行緒停頓,但是會因為占用了一部分執行緒(或者說CPU資源)而導致應用程式變慢,總吞吐量會降低,CMS默認啟 動的回收執行緒數是(CPU數量+3)/4,也就是當CPU在4個以上時,并發回收 時垃圾收集執行緒最多占用不超過25%的CPU資源,但是當CPU不足4個時(譬 如2個),那么CMS對用戶程式的影響就可能變得很大,如果CPU負載本來就 比較大的時候,還分出一半的運算能力去執行收集器執行緒,就可能導致用戶程式 的執行速度忽然降低了 50%,這也很讓人受不了,為了解決這種情況,虛擬機提 供了一種稱為“增量式并發收集器”(Incremental Concurrent Mark Sweep / i-CMS) 的CMS收集器變種,所做的事情和單CPU年代PC機作業系統使用搶占式來模 擬多任務機制的思想一樣,就是在并發標記和并發清理的時候讓GC執行緒、用戶執行緒交替運行,盡量減少GC執行緒的獨占資源的時間,這樣整個垃圾收集的程序會更長,但對用戶程式的影響就會顯得少一些,速度下降也就沒有那么明顯,但是目前版本中,i-CMS已經被宣告為“deprecated”,即不再提倡用戶使用,
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現"Concurrent Mode Failure"失敗而導致另一次Full GC的產生,由于CMS并發清理階段用戶執行緒還在運行著,伴隨程式的運行自然還會有新的垃圾不斷產生,這一部分垃圾出現在 標記程序之后,CMS無法在本次收集中處理掉它們,只好留待下一次GC時再將 其清理掉,這一部分垃圾就稱為“浮動垃圾”,也是由于在垃圾收集階段用戶線 程還需要運行,即還需要預留足夠的記憶體空間給用戶執行緒使用,因此CMS收集 器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一 部分空間提供并發收集時的程式運作使用,在默認設定下,CMS收集器在老年代 使用了 68%的空間后就會被激活,這是一個偏保守的設定,如果在應用中老年代 增長不是太快,可以適當調高引數-XX:CMSInitiatingOccupancyFraction的值來 提高觸發百分比,以便降低記憶體回收次數以獲取更好的性能,要是CMS運行期 間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了,所以說引數-XX:CMSInitiatingOccupancyFraction設定得太高將會很容易導致大最Concurrent Mode Failure"失敗,性能反而降低,

  • 還有最后一個缺點,在本節在開頭說過,CMS是款基于“標記-清除”演算法實 現的收集器,如果讀者對前面這種演算法介紹還有印象的話,就可能想到這意味著 收集結束時會產生大量空間碎片,空間碎片過多時,將會給大物件分配帶來很大 的麻煩,往往會出現老年代還有很大的空間剩余,但是無法找到足夠大的連續空 間來分配當前物件,不得不提前觸發一次Full GC.為了解決這個問題,CMS收 集器提供了一個-XX:+UseCMSCompactAtFullCollection開關引數,用于在“享 受”完Full GC服務之后額外免費附送一個碎片整理程序,記憶體整理的程序是無法并發的,空間碎片問題沒有了,但停頓時間不得不變長了,虛擬機設計者們還 提供了另外一個引數-XX: CMSFullGCsBeforeCompaction,這個引數用于設定在 執行多少次不壓縮的Full GC后,跟著來一次帶壓縮的,

??4.7 G1收集器

??G1 (Garbage First)收集器是當前收集器技術發展的最前沿成果,在JDK 1.6_ Update"中提供了 Early Access版本的G1收集器以供試用,在將來JDK 1.7正式發布 的時候,G1收集器很可能會有一個成熟的商用版本隨之發布,這里只對G1收集器進行 簡單介紹,

??G1收集器是垃圾收集器理論進一步發展的產物,它與前面的CMS收集器相比有兩 個顯著的改進:一是G1收集器是基于“標記-整理”演算法實作的收集器,也就是說它 不會產生空間碎片,這對于長時間運行的應用系統來說非常重要,二是它可以非常精確 地控制停頓,既能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收 集上的時間不得超過N毫秒,這幾乎已經是實時Java (RTSJ)的垃圾收集器的特征了,

??G1收集器可以實作在基本不犧牲吞吐量的前提下完成低停頓的記憶體回收,這是由于 它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的范圍都是整個新生代或 老年代,而G1將整個Java堆(包括新生代、老年代)劃分為多個大小固定的獨立區域 (Region),并且跟蹤這些區域里面的垃圾堆積程度,在后臺維護一個優先串列,每次根 據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First名稱的來由),區 域劃分及有優先級的區域回收,保證了 G1收集器在有限的時間內可以獲得最高的收集 效率,

??4.8 垃圾收集器引數總結

??JDK 1.6中的各種垃圾收集器到此已全部介紹完畢,在描述程序中提到了很多虛擬機非穩定的運行引數,表3-1整理了這些引數以供讀者實踐時參考,

5. 記憶體分配與回收策略

??Java技術體系中所提倡的自動記憶體管理最終可以歸結為自動化地解決了兩個問題: 給物件分配記憶體以及回收分配給物件的記憶體,關于回收記憶體這一點,我們已經使用了大量的篇幅去介紹虛擬機中的垃圾收集器體系及其運作原理,現在我們再一起來探討一下 給物件分配記憶體的那點事兒,

??物件的記憶體分配,往大方向上講,就是在堆上分配(但也可能經過JIT編譯后被拆散為標量型別并間接地在堆疊上分配,),物件主要分配在新生代的Eden區上,如果啟動 了本地執行緒分配緩沖,將按執行緒優先在TLAB±分配,少數情況下也可能會直接分配在老年代中,分配的規則并不是百分之百固定的,其細節取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中與記憶體相關的引數的設定,

??接下來我們將會講解幾條最普遍的記憶體分配規則,并通過代碼去驗證這些規則,本節中的代碼在測驗時使用Client模式虛擬機運行,沒有手工指定收集器組合,換句話 說,驗證的是使用Serial / Serial Old收集器下(ParNew / Serial Old收集器組合的規則 也基本一致)的記憶體分配和回收的策略,讀者不妨根據自己專案中使用的收集器寫一些 程式去驗證一下使用其他幾種收集器的記憶體分配策略,

??5.1 物件優先在Eden分配

??大多數情況下,物件在新生代Eden區中分配,當Eden區沒有足夠的空間進行分配 時,虛擬機將發起一次Minor GC

??虛擬機提供了 -XX:+PrintGCDetails這個收集器日志引數,告訴虛擬機在發生垃圾收集行為時列印記憶體回收日志,并且在行程退出的時候輸出當前記憶體各區域的分配情況,在實際應用中,記憶體回收日志一般是列印到檔案后通過日志工具進行分析,不過本實驗的日志并不多,直接閱讀就能看得很清楚,

??代碼清單3-3的testAllocation,方法中,嘗試分配3個2MB大小和1個4MB 大小的物件,在運行時通過-Xms20M、-Xmx20M和-Xmn10M這3個引數限制Java 堆大小為20MB,且不可擴展,其中10MB分配給新生代,剩下的10MB分配給老 年代,-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例 是8比1,從輸出的結果也能清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的資訊,新生代總可用空間為9216KB (Eden區+1個Survivor g的總 容量),

??執行testAllocation()中分配allocation4物件的陳述句時會發生一次Minor GC,這次GC的結果是新生代6945KB變為1021KB,而總記憶體占用量則幾乎沒有減少(因為 allocations 2、3三個物件都是存活的,虛擬機幾乎沒有找到可回收的物件),這次GC 發生的原因是給allocation4分配記憶體的時候,發現Eden已經被占用了 6MB,剩余空間 已不足以分配allocation4所需的4MB記憶體,因此發生Minor GC, GC期間虛擬機又發現已有的3個2MB大小的物件全部無法放入Survivor空間(Survivor空間只有1MB大小),所以只好通過分配擔保機制提前轉移到老年代去,

??這次GC結束后,4MB的allocation,物件被順利分配在Eden中,因此程式執行完 的結果是Eden占用4MB (被allocation4占用),Survivor空閑,老年代被占用6MB (被 allocation Is 2、3占用),通過GC日志可以證實這一點,

  • 新生代GC (Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具 備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快,
  • 老年代GC (Major GC / Full GC):指發生在老年代的GC,出現了 Major GC,經常 會伴隨至少一次的Minor GC (但非絕對的,在ParallelScavenge收集器的收集策略里 就有直接進行Major GC的策略選擇程序),MajorGC的速度一般會比Minor GC慢10 倍以上,
package com.intehel.demo.domain;
// vm引數 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
public class FinalizeEscapeGC {

    private static final int _1MB = 1024*1024;
    public static void testAllocation(){
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) throws Throwable {
        FinalizeEscapeGC.testAllocation();
    }
}

??運行結果:

[GC (Allocation Failure) [PSYoungGen: 6945K->1021K(9216K)] 6945K->3674K(19456K), 0.0019542 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 5440K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 53% used [0x00000000ff600000,0x00000000ffa50ed8,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffeff520,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 6748K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 65% used [0x00000000fec00000,0x00000000ff297348,0x00000000ff600000)
 Metaspace       used 3332K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

??5.2 大物件直接進入老年代

??所謂大物件就是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很 長的字串及陣列(筆者例子中的byte[]陣列就是典型的大物件),大物件對虛擬機的記憶體 分配來說就是一個壞訊息(替Java虛擬機抱怨一句,比遇到一個大物件更加壞的訊息就是 遇到一群“朝生夕滅”的“短命大物件",寫程式的時候應當避免),經常出現大物件容易 導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來"安置”它們,

??虛擬機提供了一個-XX:PretenureSizeThreshold引數,令大于這個設定值的物件直接 在老年代中分配,這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內 存拷貝(復習一下:新生代采用復制演算法收集記憶體),

??執行代碼清單3-4中的testPretenureSizeThreshold()方法后,我們看到老年代10MB的空間被使用了 40%,也就是4MB的allocation 物件直接就分配在老年代中,這是因為PretenureSizeThreshold被設定為3MB (就是 3145728B,這個引數不能與-Xmx之類的引數一樣直接寫3MB),因此超過3MB的對 象都會直接在老年代中進行分配,

package com.intehel.demo.domain;
//vm引數: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728
public class FinalizeEscapeGC {

    private static final int _1MB = 1024*1024;
    public static void testPretenured(){
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    }
    public static void main(String[] args) throws Throwable {
        FinalizeEscapeGC.testPretenured();
    }
}
Heap
 PSYoungGen      total 9216K, used 4893K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 59% used [0x00000000ff600000,0x00000000ffac76b8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
 Metaspace       used 3189K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

??5.3 長期存活的物件將進入老年代

??虛擬機既然采用了分代收集的思想來管理記憶體,那記憶體回收時就必須能識別哪些對 象應當放在新生代,哪些物件應放在老年代中,為了做到這點,虛擬機給每個物件定義 了一個物件年齡Age)計數器,如果物件在Eden出生并經過第一次Minor GC后仍然 存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并將物件年齡設為1, 物件在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定 程度(默認為15歲)時,就會被晉升到老年代中,物件晉升老年代的年齡閾值,可以通過MaxTenuringThreshold= 15的引數設定,

??可以試試分別以MaxTenuringThreshold= 1 和MaxTenuringThreshold= 15,此方法中物件需要256KB的記憶體空間,Survivor空間可以容納,當MaxTenuringThreshold=1 時,allocationl物件在第二次GC發生時進入老年代,新生代已使用的記憶體GC后 會非常干凈地變成0KB,而MaxTenuringThreshold=15時,第二次GC發生后, allocationl物件則還留在新生代Survivor空間,這時候新生代仍然有404KB的空間被占用,

package com.intehel.demo.domain;

public class FinalizeEscapeGC {
//vm配置 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1
    @SuppressWarnings("unused")
    private static final int _1MB = 1024*1024;
    public static void testAllocation(){
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }

    public static void main(String[] args) throws Throwable {
        FinalizeEscapeGC.testAllocation();
    }
}

以MaxTenuringThreshold= 1 的引數設定來運行的結果:

[GC (Allocation Failure) [PSYoungGen: 5153K->1021K(9216K)] 13345K->10054K(19456K), 0.0016619 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1021K->0K(9216K)] [ParOldGen: 9032K->5879K(10240K)] 10054K->5879K(19456K), [Metaspace: 3309K->3309K(1056768K)], 0.0058565 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 4262K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 52% used [0x00000000ff600000,0x00000000ffa29b48,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 5879K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 57% used [0x00000000fec00000,0x00000000ff1bdf00,0x00000000ff600000)
 Metaspace       used 3333K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

以MaxTenuringThreshold= 15的引數設定來運行的結果:

[GC (Allocation Failure) [PSYoungGen: 5153K->1021K(9216K)] 13345K->10036K(19456K), 0.0019170 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1021K->0K(9216K)] [ParOldGen: 9015K->5879K(10240K)] 10036K->5879K(19456K), [Metaspace: 3310K->3310K(1056768K)], 0.0058862 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 4178K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 51% used [0x00000000ff600000,0x00000000ffa14930,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 5879K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 57% used [0x00000000fec00000,0x00000000ff1bdf00,0x00000000ff600000)
 Metaspace       used 3317K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 356K, capacity 388K, committed 512K, reserved 1048576K

??5.4 動態物件年齡判定

??為了能更好地適應不同程式的記憶體狀況,虛擬機并不總是要求物件的年齡必須達到 MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小 的總和大于Survivor空間的一半,年齡大于或等于該年齡的物件就可以直接進入老年代,

??執行代碼清單3-6中的testTenuringThreshold2()方法,并設定引數-XX: MaxTenuringThreshold=15,會發現運行結果中Survivor的空間占用仍然為0%,而老年 代比預期增加了 6%,也就是說allocation1, allocation2物件都直接進入了老年代,而沒 有等到15歲的臨界年齡,因為這兩個物件加起來已經達到了 512KB,并且它們是同年 的,滿足同年物件達到Survivor空間的一半規則,我們只要注釋掉其中一個物件的new 操作,就會發現另外一個不會晉升到老年代中去了,

package com.intehel.demo.domain;

public class FinalizeEscapeGC {
    @SuppressWarnings("unused")
    private static final int _1MB = 1024*1024;
    public static void testAllocation(){
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) throws Throwable {
        FinalizeEscapeGC.testAllocation();
    }
}
[GC (Allocation Failure) [PSYoungGen: 5409K->992K(9216K)] 13601K->10298K(19456K), 0.0023467 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 992K->0K(9216K)] [ParOldGen: 9306K->6159K(10240K)] 10298K->6159K(19456K), [Metaspace: 3322K->3322K(1056768K)], 0.0068674 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 4178K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 51% used [0x00000000ff600000,0x00000000ffa14930,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 6159K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 60% used [0x00000000fec00000,0x00000000ff203f80,0x00000000ff600000)
 Metaspace       used 3331K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

??5.5 空間分配擔保

??在發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大于 老年代的剩余空間大小,如果大于,則改為直接進行一次Full GC,如果小于,則査看 HandlePromotionFailure設定是否允許擔保失敗;如果允許,那只會進行Minor GC ;如果不允許,則也要改為進行一次Full GC,

??前面提到過,新生代使用復制收集演算法,但為了記憶體利用率,只使用其中一個 Survivor空間來作為輪換備份,因此當出現大量物件在Minor GC后仍然存活的情況時 (最極端就是記憶體回收后新生代中所有物件都存活),就需要老年代進行分配擔保,讓 Survivor無法容納的物件直接進入老年代,與生活中的貸款擔保類似,老年代要進行這 樣的擔保,前提是老年代本身還有容納這些物件的剩余空間,一共有多少物件會活下 來,在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老 年代物件容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行 Full GC來讓老年代騰出更多空間,

??取平均值進行比較其實仍然是一種動態概率的手段,也就是說如果某次Minor GC存活后的物件突增,遠遠高于平均值的話,依然會導致擔保失敗(Handle Promotion Failure)?如果出現了 HandlePromotionFailure失敗,那就只好在失敗后 重新發起一次Full GC,雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還 是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁

??HandlePromotionFailure = false引數和MaxTenuringThreshold= true在JDK8及以后不再支持

??5.6 本章小結

??本章介紹了垃圾收集的演算法、幾款JDK 1.6中提供的垃圾收集器特點及其運作原理, 通過代碼實體驗證了 Java虛擬機中自動記憶體分配及回收的主要規則,

??記憶體回收與垃圾收集器在很多時候都是影響系統性能、并發能力的主要因素之 一,虛擬機之所以提供多種不同的收集器及大量的調節引數,是因為只有根據實際應 用需求、實作方式選擇最優的收集方式才能獲取最好的性能,沒有固定收集器、引陣列合,也沒有最優的調優方法,虛擬機也沒有什么必然的記憶體回收行為,因此學習虛擬機記憶體知識,如果要到實踐調優階段,必須了解每個具體收集器的行為、優勢和劣勢、調節引數,在接下來的兩章中,作者將會介紹記憶體分析的工具和調優的一些具體案例,

??

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502178.html

標籤:Java

上一篇:IOS OpenGL ES GPUImage 差值混合 GPUImageDifferenceBlendFilter

下一篇:bbs專案之登錄功能

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more