1. 程式人生 > >java垃圾收集器和記憶體分配回收策略

java垃圾收集器和記憶體分配回收策略

概述

GC要完成3件事:

  1. 哪些記憶體需要回收?
  2. 什麼時候回收?
  3. 如何回收?

Java記憶體執行時區域的各部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著入棧和出棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束,記憶體自然就跟隨著回收了。

而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收是動態的,垃圾收集器所關注的是這部分的記憶體。

物件存活判斷

引用計數演算法

給物件新增一個引用計數器,每當有一個地方引用它的地方,計數器值+1;當引用失效,計數器值就減1;任何時候計數器為0,物件就不可能再被引用了。

它很難解決物件之間相互迴圈引用的問題。

虛擬機器並不是通過引用計數演算法來判斷物件是否存活的

可達性分析演算法

在主流的商用語言(Java、C#)中都使用可達性分析(Reachability Analysis)來判定物件是否存活的。

通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

可達性分析

object5 6 7 對於GC Roots是不可達的,所以會被判定為回收物件

在Java語言中,可以作為Gc Roots的物件包括下面幾種:

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

再談引用

在JDK 1.2 之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  1. 強引用就是指在程式程式碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。
  2. 軟引用是用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。
  3. 弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK 1.2之後,提供了WeakReference類來實現弱引用。
  4. 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。

生存還是死亡?

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。

如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。下面例子可以看出finalize()被執行,但是它仍然可以存活。

package cc.wsyw126.java.garbageCollection;

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 method executed!");
        SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        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 method executed!
yes, I am still alive :)
no, i am dead :(

一樣的程式碼,一次逃脫,一次失敗。因為物件的finalize()只能被系統執行一次。

建議大家儘量避免使用它,因為它不是C/C++中的解構函式,而是Java剛誕生時為了使C/C++程式設計師更容易接受它所做出的一個妥協。它的執行代價高昂,不確定性大,無法保證各個物件的呼叫順序。

回收方法區

在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。

永久代的垃圾回收主要回收兩部分內容:廢棄常量和無用的類。“廢棄常量”判斷比較簡單,但是“無用的類”的判斷複雜一些,需要滿足下面3個條件:

  1. 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
  2. 載入該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

是否對類進行回收,HotSpot虛擬機器提供了-Xnoclassgc引數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading, -XX:+TraceClassUnLoading檢視類架子啊和解除安裝資訊,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機器中使用,-XX:+TraceClassUnLoading引數需要FastDebug版的虛擬機器支援。

在大量使用反射、動態代理、Cglib等ByteCode框架、動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛擬機器具備類解除安裝的功能,以保證永久代不會溢位。

垃圾收集演算法

標記-清除演算法

如同它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。標記—清除演算法的執行過程如下圖所示:

標記-清除演算法

複製演算法

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

複製演算法

現在的商業虛擬機器都採用這種收集演算法來回收新生代,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)

分配擔保:如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。

標記-整理演算法

標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,“標記-整理”演算法的示意圖如圖所示。

標記-整理演算法

分代收集演算法

根據物件存活週期的不同將記憶體分為幾塊。一般把Java堆分為新生代和老年代,根據各個年代的特點採用最合適的收集演算法。在新生代中,每次垃圾收集時有大批物件死去,只有少量存活,可以選用複製演算法。而老年代物件存活率高,使用標記清除或者標記整理演算法。

HotSpot的演算法實現

列舉根節點

從可達性分析中從GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全域性性的引用(例如常量或類靜態屬性)與執行上下文(例如幀棧中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多時間。

另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行–這裡“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執行執行緒(Sun將這件事情稱為“Stop The World”)的其中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點也是必須要停頓的。

由於目前的主流Java虛擬機器使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知哪些地方存放著物件的引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的,在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些資訊了。

安全點

在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots列舉,但一個很現實的問題隨之而來:可能導致引用關係變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。

實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些資訊,這些位置稱為安全點(Safepoint),即程式執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定即不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過分增大執行時負荷。所以,安全點的選定基本上是以程式“是否具有讓程式長時間執行的特徵”為標準進行選定的–因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這個原因而過長時間執行,“長時間執行”的最明顯特徵就是指令序列複用,例如方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生Safepoint。

對於Safepoint,另一個需要考慮的問題是如何在GC發生時讓所以執行緒(這裡不包括執行JNI呼叫的執行緒)都“跑”到最近的安全點上再停頓下來。這裡有兩種方案可供選擇:

  1. 搶先式中斷(Preemptive Suspension)
  2. 主動式中斷(Voluntary Suspension)

其中搶先式中斷不需要執行緒的執行程式碼主動去配合,在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它“跑”到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件。

而主動式中斷的思想是當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立物件需要分配記憶體的地方。

安全區域

使用Safepoint似乎已經完美地解決了如何進入GC的問題,但實際情況卻並不一定。Safepoint機制保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程式就”不執行“的時候呢?所謂的程式不執行就是沒有分配CPU時間,典型的例子就是執行緒處於Sleep狀態或者Blocked狀態,這時候執行緒無法響應JVM的中斷請求,”走“到安全的地方去中斷掛起,JVM也顯然不太可能等待執行緒重新被分配CPU時間。對於這種情況,就需要安全區域(Safe Region)來解決。

安全區域是指在一段程式碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴充套件了的Safepoint。

線上程執行到Safe Region中的程式碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裡JVM要發起GC時,就不用管標識自己為Safe Region狀態的執行緒了。線上程要離開Safe Region時,它要檢查系統是否已經完成了根節點列舉(或者是整個GC過程),如果完成了,那執行緒就繼續執行,否則它就必須繼續等待直到收到可以安全離開Safe Region的訊號為止。

垃圾收集器

垃圾收集器

Serial Collecor

Serial收集器是單執行緒收集器,是分代收集器。它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。

  1. 新生代:單執行緒複製收集演算法;
  2. 老年代:單執行緒標記整理演算法。

Serial一般在單核的機器上使用,是Java 5非服務端JVM的預設收集器,引數-XX:UseSerialGC設定使用。

ParNew收集器

ParNew收集器和Serial收集器的主要區別是

  1. 新生代的收集,一個是單執行緒一個是多執行緒。
  2. 老年代的收集和Serial收集器是一樣的。

實際上是Serial收集器的多執行緒版本,擁有可控制引數(如:-XX:SurvivorRatio, -XX:PretenureSizeThreshold, -XX:HandlePromotionFailure等),收集演算法,停頓,物件分配規則,回收策略都和Serial收集器完全一樣。

ParNew收集器是許多執行在server模式下的虛擬機器中首選的新生代收集器,一個重要的原因是,只有ParNew和Serial收集器能和CMS收集器共同工作。無法與JDK1.4中存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew和Serial。

ParNew收集器是使用-XX:+UseConcMarkSweepGC選項的預設新生代收集器。也可以用-XX:+UseParNewGC選項來強制指定它。

ParNew收集器在單CPU環境中不比Serial效果好,甚至可能更差,兩個CPU也不一定跑的過,但隨著CPU數量的增加,效能會逐步增加。預設開啟的收集執行緒數與CPU數量相同。在CPU數量很多的情況下,可以使用-XX:ParallelGCThreads引數來限制執行緒數。

Parallel並行: 多垃圾收集執行緒並行工作,但使用者執行緒仍需等待

Concurrent併發:使用者執行緒和垃圾收集同時進行。

Parallel Scavenge收集器

同ParNew一樣是使用複製演算法的新生代並行多執行緒收集器。

Parallel Scavenge的特點是它的關注點與其他收集器不同,CMS等收集器的關注點儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於執行使用者程式碼與CPU總消耗時間的比值。

高吞吐量和停頓時間短的策略相比,主要強調任務更快完成,而後者強呼叫戶互動體驗。

Parallel Scavenge提供兩個引數控制垃圾回收停頓時間:-XX:MaxGCPauseMillis和-XX:GCTimeRatio

  1. MaxGCPauseMillis允許的值是一個大於零的毫秒數,收集器將盡力保證記憶體回收話費的時間不超過設定值。GC停頓時間縮小是以犧牲吞吐量和新生代空間來換取的,也就是要使停頓時間更短,垃圾回收的頻率會增加。
  2. GCTimeRatio的值是一個大於0小於100的整數,也就是垃圾收集時間佔總時間的比率。設為19,則允許最大GC時間就佔總時間的5%(1/(1+19)),預設99.

Parallel Scavenge收集器也被稱為吞吐量優先收集器。

還有一個引數, -XX:+UseAdaptiveSizePolicy,是個開關引數,開啟後會自動調整Eden/Survivor比例,老年代物件年齡,新生代大小等。這個引數也是Parallel Scavenge和ParNew的重要區別。

Serial Old收集器

是Serial的老年代版本,同樣是單執行緒收集器,使用標記-整理演算法。主要是client模式下的虛擬機器使用

兩大用途:

  1. 在JDK1.5及之前的版本中與Parallel Scavenge搭配使用。
  2. 作為CMS收集器的後備預案。在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多執行緒和標記-整理演算法。在JDK1.6中才開始使用。由於之前的版本中,Parallel Scavenge只有使用Serial Old作為老年代收集器,其吞吐量優先的設計思路不能被很好的貫徹,在Parallel Old收集器出現後,這兩者的配合主要用於貫徹這種思路。

CMS收集器

Concurrent Mark Sweep 以獲取最短回收停頓時間為目標的收集器,比較理想的應用場景是B/S架構的伺服器。

基於標記-清除演算法實現,執行過程分成4個步驟:

  1. 初始標記(需要stop the world),標記一下GC Roots能直接關聯到的物件,速度很快。
  2. 併發標記,進行GC Roots Tracing的過程。
  3. 重新標記(需要stop the world),為了修正併發標記時使用者繼續執行而產生的標記變化,停頓時間比初始標記長,遠比並發標記短。
  4. 併發清除

缺點:

  1. CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是因為佔用了一部分CPU資源而導致應用程式變慢,總吞吐量就會降低。CMS預設啟動的回收執行緒數為(CPU數量+3)/4。為了解決這一情況,有一個變種i-CMS,但目前並不推薦使用。
  2. CMS收集器無法處理浮動垃圾(floating garbage).可能會出現concurrent mode failure導致另一次full gc的產生。在CMS的併發清理階段,由於程式還在執行,垃圾還會不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理掉它們,只好留到下一次GC再處理。這種垃圾稱為浮動垃圾。同樣由於CMS GC階段使用者執行緒還需要執行,即還需要預留足夠的記憶體空間供使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被灌滿了再進行收集而需要預留一部分空間提供併發收集時的程式運作使用。預設設定下,CMS收集器在老年代使用了68%的空間後就會被啟用。這個值可以用-XX:CMSInitiatingOccupancyFraction來設定。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現concurrent mode failure,這時候就會啟用Serial Old收集器作為備用進行老年代的垃圾收集。
  3. 空間碎片過多(標記-清除演算法的弊端),提供-XX:+UseCMSCompactAtFullCollection引數,應用於在FULL GC後再進行一個碎片整理過程。-XX:CMSFullGCsBeforeCompaction,多少次不壓縮的full gc後來一次帶壓縮的。

G1收集器

G1. Garbage first,尚在研發階段,使用標記-整理演算法,精確控制停頓,極力避免全區域垃圾收集。前面的收集器進行的收集範圍都是整個新生代或老年代,而G1將整個JAVA堆劃分為多個大小固定的獨立區域,跟蹤這些區域裡面的垃圾堆積程度,在後臺維護一個優先列表,每次在允許的收集時間裡,優先回收垃圾最多的區域。

理解GC日誌

[GC [PSYoungGen: 8987K->1016K(9216K)] 9984K->5056K(19456K), 0.0569611 secs] [Times: user=0.03 sys=0.02, real=0.06 secs] 
[GC [PSYoungGen: 8038K->1000K(9216K)] 12078K->10425K(19456K), 0.0709523 secs] [Times: user=0.05 sys=0.00, real=0.07 secs] 
[Full GC [PSYoungGen: 1000K->0K(9216K)] [ParOldGen: 9425K->8418K(10240K)] 10425K->8418K(19456K) [PSPermGen: 9678K->9675K(21504K)], 0.3152834 secs] [Times: user=0.39 sys=0.00, real=0.32 secs] 
[Full GC [PSYoungGen: 8192K->3583K(9216K)] [ParOldGen: 8418K->9508K(10240K)] 16610K->13092K(19456K) [PSPermGen: 9675K->9675K(22016K)], 0.1913859 secs] [Times: user=0.34 sys=0.00, real=0.19 secs] 
[Full GC [PSYoungGen: 7716K->7702K(9216K)] [ParOldGen: 9508K->9508K(10240K)] 17224K->17210K(19456K) [PSPermGen: 9675K->9675K(21504K)], 0.2769775 secs] [Times: user=0.52 sys=0.00, real=0.28 secs] 
[Full GC [PSYoungGen: 7702K->7702K(9216K)] [ParOldGen: 9508K->9409K(10240K)] 17210K->17111K(19456K) [PSPermGen: 9675K->9675K(21504K)], 0.2491993 secs] [Times: user=0.64 sys=0.00, real=0.25 secs]
  1. “[GC”和“[full DC”說明了這次垃圾回收的停頓型別。如果是呼叫System.gc()方法所觸發的收集,那麼這裡顯示“[Full DC(System)”.
  2. [DefNew、[Tenured、[Perm 表示GC發生的區域。如果是ParNew收集器,新生代名為“[ParNew”.如果採用Parallel Scavenge收集器,那它配套的新生代名為”[PSYoungGen”。對於老年代和永久代同理。
  3. [PSYoungGen: 8987K->1016K(9216K)] 9984K->5056K(19456K), 0.0569611 secs]中後面的數字含義是:GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該區域總容量)。而方括號之外的表示“GC前Java堆已經使用的容量 -> GC後Java堆已經使用的容量(Java堆總容量)”。後面的時間是該區域GC所佔用的時間,單位是秒。
  4. [Times: user=0.03 sys=0.02, real=0.06 secs]這裡的user、sys和real與Linux的time命令所輸出的時間含義一,分別代表使用者態消耗的CPU時間,核心態消耗的CPU時間和操作從開始到結束所經過的牆鍾時間。

垃圾收集器引數總結

參  數描  述
UseSerialGC虛擬機器執行在Client模式下的預設值,開啟此開關後,使用Serial + Serial Old的收集器組合進行記憶體回收
UseParNewGC開啟此開關後,使用ParNew + Serial Old的收集器組合進行記憶體回收
UseConcMarkSweepGC開啟此開關後,使用ParNew + CMS + Serial Old的收集器組合進行記憶體回收。Serial Old收集器將作為CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用
UseParallelGC虛擬機器執行在Server模式下的預設值,開啟此開關後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行記憶體回收
UseParallelOldGC開啟此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行記憶體回收
SurvivorRatio新生代中Eden區域與Survivor區域的容量比值,預設為8,代表Eden∶Survivor=8∶1
PretenureSizeThreshold直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配
MaxTenuringThreshold晉升到老年代的物件年齡。每個物件在堅持過一次Minor GC之後,年齡就增加1,當超過這個引數值時就進入老年代
UseAdaptiveSizePolicy動態調整Java堆中各個區域的大小以及進入老年代的年齡
HandlePromotionFailure是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有物件都存活的極端情況
ParallelGCThreads設定並行GC時進行記憶體回收的執行緒數
GCTimeRatioGC時間佔總時間的比率,預設值為99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效
MaxGCPauseMillis設定GC的最大停頓時間。僅在使用Parallel Scavenge收集器時生效
CMSInitiatingOccupancyFraction設定CMS收集器在老年代空間被使用多少後觸發垃圾收集。預設值為68%,僅在使用CMS收集器時生效
UseCMSCompactAtFullCollection設定CMS收集器在完成垃圾收集後是否要進行一次記憶體碎片整理。僅在使用CMS收集器時生效
CMSFullGCsBeforeCompaction設定CMS收集器在進行若干次垃圾收集後再啟動一次記憶體碎片整理。僅在使用CMS收集器時生效

記憶體分配與回收策略

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

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

物件優先在Eden分配

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

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

下面程式碼的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區的總容量)。

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

這次GC結束後,4MB的allocation4物件順利分配在Eden中,因此程式執行完的結果是Eden佔用4MB(被allocation4佔用),Survivor空閒,老年代被佔用6MB(被allocation1、allocation2、allocation3佔用)。通過GC日誌可以證實這一點。

相關推薦

java垃圾收集記憶體分配回收策略

概述GC要完成3件事:哪些記憶體需要回收?什麼時候回收?如何回收?Java記憶體執行時區域的各部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著入棧和出棧操作。每一個棧幀中分配多少記憶體基本上是在類

Java虛擬機器】垃圾收集記憶體分配策略

垃圾收集器和記憶體分配策略 垃圾收集器 Serial 收集器 ParNew 收集器 Parallel Scavenge 收集器 Serial Old 收集器 Parallel Old 收集器 CMS 收集器(C

JVM面試題整理-Java記憶體區域與記憶體溢位異常、垃圾收集記憶體分配策略

1、Java虛擬機器記憶體(執行時資料區域)的劃分,每個區域的功能 關於JVM 執行時記憶體劃分的例項參考: http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&

虛擬機器學習之二:垃圾收集記憶體分配策略

1.物件是否可回收 1.1引用計數演算法 引用計數演算法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時候計數器值為0的物件就是不可能再被使用的物件。 客觀來說,引用計數演算法的實現簡單,判定效率高,在大部分情況下都是

JAVA垃圾收集記憶體分配策略

3.1 概述 LISP是第一門使用記憶體動態分配和垃圾收集技術的語言。 CG需要完成的三件事: 1、哪些記憶體需要回收? 2、什麼時候回收? 3、如何回收? JAVA堆和方法區中,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一

垃圾收集記憶體分配策略 JVM筆記2

目錄 物件已死嗎 引用計數演算法 可達性分析演算法 再談引用 生存還是死亡 回收方法區 垃圾回收演算法 標記-清除演算法 複製演算法 標記-整理演算法 分代收集演算法 HotSpot的演算法實現 垃圾收集器 Serial收集器 ParNew

深入理解Java虛擬機器總結一垃圾收集記憶體分配策略(二)

深入理解Java虛擬機器總結一垃圾收集器與記憶體分配策略(二) 垃圾回收概述 如何判定物件為垃圾物件 垃圾回收演算法 垃圾收集器詳解 記憶體分配策略 垃圾回收概述 如何判定物件為垃圾物件 引用計數法: 在物件

《深入理解JAVA虛擬機器》詳細解讀(第三章 ):垃圾收集記憶體分配策略

  目錄 一、垃圾收集器與記憶體分配策略 1 概述 2 物件已經死亡? 2.1引用計數法(未使用) 2.2可達性分析演算法 2.3 再談引用 2.4 生存還是死亡 2.5 回收方法區 3 垃圾收集演算法 3.1 複製演算法(Copy) 3

Java虛擬機器筆記-1(Java技術體系&自動記憶體管理機制&記憶體區域與記憶體溢位&垃圾收集記憶體分配策略

世界上沒有完美的程式,但寫程式是不斷追求完美的過程。 Devices(裝置、裝置)、GlassFish(商業相容應用伺服器) 目錄 1. Java技術體系包括: Java技術體系的4個平臺 虛擬機器分類 HotSpot VM 模組化、混合程式設計 多核並行

深入理解Java虛擬機器——垃圾收集記憶體分配策略(讀書筆記)

判斷物件是否存活 1、引用計數法 給物件新增一個引用計數器,每當有一個地方引用它時,計數器值加1,當引用失效時,計數器值減1, 任何時刻計數器為0的物件就是不可能再被使用的。 缺點:不能解決物件之間迴圈引用的問題 2、根搜尋演算法(GC Roots Tracing)

深入理解Java虛擬機器讀書筆記2----垃圾收集記憶體分配策略

二 垃圾收集器與記憶體分配策略 1 JVM中哪些記憶體需要回收?     JVM垃圾回收主要關注的是Java堆和方法區這兩個區域;而程式計數器、虛擬機器棧、本地方法棧這3個區域隨執行緒而生,隨執行緒而滅,隨著方法結束或者執行緒結束記憶體自然

JVM垃圾收集記憶體分配策略(總結自《深入理解Java虛擬機器》)

1、物件可用性判斷 垃圾收集器在回收物件前,需要判斷哪些物件沒有被廢棄,哪些物件已經廢棄了(即無法通過任何途徑使用的物件)。所以,垃圾收集器需要一種演算法來判定這個物件是否需要回收。 (1)引用計數演算法 引用計數演算法的基本思想是給一個物件新增一個引用計數器,

垃圾收集記憶體分配策略(六)——記憶體分配回收策略

物件的記憶體分配,往大方向上講,就是在堆上分配(但也可能經過JIT編譯後被拆散為標量型別並間接地棧上分配),物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配。少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,

《深入理解Java虛擬機器》學習筆記之垃圾收集記憶體分配策略

一、概述 GC(Garbage Collection)需要完成的三件事 (1)哪些記憶體需要回收 (2)什麼時候回收 (3)如何回收 GC主要面向Java堆和方法區中的記憶體 原因:這部份

深入理解Java虛擬機器 讀書筆記——垃圾收集記憶體分配策略

第3章 垃圾收集器與記憶體分配策略 關於Java中的引用型別 強引用(Strong Reference):Object obj = new Object(); 這樣的常規引用,只要引用還在,就永遠不會回收物件。 軟引用(Soft Reference):

Java虛擬機器記憶體管理(二)--垃圾收集記憶體分配策略

概述     Java記憶體執行時區域的各個部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的(儘管在執行期會由JIT編

深入理解java虛擬機器-垃圾收集記憶體分配策略

開發十年,就只剩下這套架構體系了! >>>   

JVM的垃圾收集機制記憶體分配策略

首先給大家看一下JVM的資料區模型。 上圖是JVM的資料區模型。但是在Hotspot JVM中,我們知道執行時常量是屬於方法區的,而方法區又屬於堆。對於棧,在hotspot中虛擬機器棧和本地棧是合二為一的。 這裡在順便說一說虛擬機器物件的結構,如下圖所示

第三章垃圾收集記憶體分配策略

3.2物件死亡的判斷方法 3.2.1引用計數法 給物件新增一個引用計數器,每當一個地方引用它就+1,引用失效就-1,當計數器為0時就表示物件已經死亡。 缺點是無法解決迴圈引用問題 3.2.2可達性分析 將GC root作為根節點向下遍歷,無法遍歷到的物件(GC Root到這個物件不可達)就表示該物件

JVM(三) 垃圾收集記憶體分配策略

一 重點關注的資料區域: 堆 和 方法區 Java記憶體執行時區域中的程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒生,隨執行緒滅;每 一個棧幀中分配多少記憶體是在類結構確定下來就已知的,因此這幾個區域的記憶體分配和回收都 具備確定性. 二 垃圾回收