深入理解 Java 虛擬機(六)~ Garbage Collection 剖析

Java 虛擬機系列文章目錄導讀:

深入理解 Java 虛擬機(一)~ class 字節碼文件剖析
深入理解 Java 虛擬機(二)~ 類的加載過程剖析
深入理解 Java 虛擬機(三)~ class 字節碼的執行過程剖析
深入理解 Java 虛擬機(四)~ 各種容易混淆的常量池
深入理解 Java 虛擬機(五)~ 對象的創建過程
深入理解 Java 虛擬機(六)~ Garbage Collection 剖析

前言

Java 虛擬機中的垃圾回收相關的知識點非常多也非常複雜,但是 理解 Java 虛擬機中的垃圾回收相關的知識對於理解和開發出高質量的程序還是很有裨益的

本文主要內容:

  • 什麼樣的對象可以被回收?
    • 引用計數算法
    • 可達性分析算法
    • 什麼對象可以作爲 GC Roots
  • 垃圾回收的基礎算法及小結
  • 爲什麼要分代回收
  • 對象什麼時候進入老年代
  • 分代回收的執行過程
  • 什麼時候會觸發 Major GC
  • PermGen VS Metaspace
  • Compressed Class Space
    • Instrumentation 計算對象佔用內存的大小
    • JOL 查看 Java 對象內存佈局
  • Code Cache
  • 理解 OutOfMemoryError 異常
  • JVM 垃圾收集器
    • 垃圾收集器相關術語
    • 7 種收集器詳解
    • G1 收集器及最佳實踐
  • Java 7, Java 8, Java 9 垃圾回收的變化
    • JDK 7 Changes
    • JDK 8 Changes
    • JDK 9 Changes

什麼樣的對象可以被回收?

在垃圾清理之前首先要做的事情就是確定哪些對象可以被回收。確定對象是否可以被回收主要有兩種方案:引用計數算法、可達性分析算法。

引用計數算法

引用計數算法的原理是爲對象添加一個引用計數器,每當有一個地方引用該對象,那麼該對象的引用計數器加 1 ;當引用失效時,引用計數器就減 1

如果一個對象的引用計數器爲 0 ,那麼該對象就可以被清理回收了。像 Python 語言就是引用計數算法來進行內存管理的。

但是主流的 Java 虛擬機沒有選用引用計算法來管理內存,主要的原因在於它很難解決對象之間循環引用的問題。

可達性分析算法

在主流的商用語言,如 Java、C# 主流的實現中,都是通過可達性分析來判斷對象是否存活的。可達性分析算法的基本思想是通過一系列稱爲 GC Roots 的對象作爲起點。從這些節點往下搜索,搜索所做過的路徑稱之爲引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連,則證明此對象不可用,可以被回收了。如下圖所示:

GC Roots

那麼什麼對象可以作爲 GC Roots 對象呢?

根據 Eclipse 對 GC Root 的描述,垃圾收集根是一個可以從堆外部訪問的對象。以下原因使得對象成爲GC根:

  • System Class

    被 Bootstrap ClassLoader 加載的類(rt.jar)

  • JNI Local

    Native 代碼中的局部變量

  • JNI Global

    Native 代碼中的全局變量(Global variable)

  • Thread Block

    從當前活動的線程塊引用的對象。

  • Thread

    開始,但沒有停止的線程。

  • Busy Monitor

    調用了 wait(), notify()方法的對象,或者 synchronize 的鎖對象

  • Java Local

    局部變量. 例如,方法入參或方法中創建的本地變量仍然在線程的棧中

  • Native Stack

  • Finalizable

    在 finalizer queue 中的等待被 finalize 的對象

  • Unfinalized

    一個擁有 finalize 方法的對象,但是還沒有被 finalized 並且不在 finalizer queue 中

  • Unreachable

    從任何其他根中都無法訪問的對象,但是 MAT 將其標記爲根,以保留不包含在分析中的對象。

  • Java Stack Frame

垃圾回收的基礎算法

標記-清除算法

標記清除(Mark Sweep) 算法分爲 標記清除 兩個階段。

標記 - 清除算法是最基礎的收集算法,後續的收集算法都是基於這種思路進行改造的。

原理:標記階段會標記出需要回收的對象,標記完成後統一回收所有被標記的對象。

不足

  • 1、效率不高。標記和清除的兩個過程的效率都不高;
  • 2、空間問題。標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致導致程序運行過程中分配大對象時,無法找到足夠的連續內存而不得不觸發另一次垃圾收集動作。

標記清除算法執行過程如下圖所示:

gc-mark-sweep

複製算法

由於標記清除算法的效率不高和內存碎片化問題,複製(Copying)算法就出現了。

原理:將可用內存平均分爲 2 塊,每次只使用其中的一塊。當這塊內存使用完了,就將還存活的對象複製到另一塊內存裏,然後統一回收剛剛用完的那塊內存。

例如將可用內存劃分爲 A、B 兩塊,當 A 使用完畢後,會將 A 中存活對象複製到 B 塊內存中,然後把 A 內存統一回收掉,如下圖所示:

gc-copying

優點:效率比標記清除算法好,也不會出現內存碎片的情況

缺點:

  • 內存利用率不高。將內存平均分成 2 塊,可用的內存就變成了原來的一半
  • 如果對象的存活率比較高的話,複製的操作就會比較頻繁

標記整理算法

由於複製算法對於存活率高的內存進行垃圾收集需要頻繁的複製操作,而標記-清除算法又會造成內存碎片化。所以有人提出了 標記-整理(Mark Compact)算法。

標記-整理將存活對象都向一端移動,然後清理掉存活對象邊界以外的內存。如下圖所示:將存活對象都向一端移動,然後清理掉存活對象邊界以外的內存。如下圖所示:

GC-Mark-Compact

分代收集算法

當前商業虛擬機垃圾收集器都採用 “分代收集(Generational Collection)” 算法。

這種算法的主要思想是:根據對象的存活週期的不同將內存劃爲幾塊,一般是把 Java 堆分爲 新生代(Young Generation)和老年代(Old Generation)

在新生代中,每次垃圾收集都會發現大量對象死去,只有少量對象存活,那就可以使用複製算法。只需要付出少量的複製成本就可以完成收集。

在老年代中,對象的存活率高,由於複製收集算法在對象存活較高時需要更多的複製操作,效率將會變低,所以在老年代不適合使用複製算法,一般使用 標記-清理標記-整理 算法。

發生在新生代的 GC 稱之爲 Minor GC

發生在老年代的 GC 稱之爲 Major GC 或 Full GC,在執行 Major GC 之前也有可能會先執行 Minor GC

新生代由 1 個 Eden 區和 2 個 Survivor 區組成,如下圖所示:

young generation area

在 Hotspot 虛擬機中 1 個 Eden 區和 2 個 Survivor 區它們之間的比例關係爲 8:1:1

每次使用 Eden 和其中一塊 Survivor 空間,最後清理 Eden 和剛剛使用過的 Survivor 空間。

垃圾回收基礎算法小結

垃圾回收的基礎算法是後面算法改進的基礎,下面對這幾種算法的優缺點做一個小結:

算法 優點 缺點
複製 吞吐量達(一次能回收整個空間),分配效率高(對象可連續分配),沒有內存碎片 堆的使用效率低(需要額外的一個空間 To Space),需要移動對象
標記清除 無須移動對象,算法簡單 內存碎片化,分配慢(需要找到一個合適的空間)
標記整理 堆的使用效率高,無內存碎片 暫停時間更長,對緩存不友好(對象移動後,順序關係不存在)
分代 組合算法,分配效率高,堆的使用效率高 算法複雜

爲什麼要分代回收

早期沒有進行分代的時候,虛擬機需要爲所有的對象進行標記(marking)和壓縮(compact)。隨着越來越多的對象的創建,導致垃圾回收的時間越來越長。但是經過數據分析表明,絕大部分的對象生命週期是非常短的。

例如下面一張圖,縱座標表示內存分配的字節數,橫座標表示隨着時間的推移內存分配的字節數:

在這裏插入圖片描述

所以如果每次垃圾回收都對整個堆進行標記和壓縮,那麼垃圾回收的效率就會變得很低。

對象分配在 Eden 區,隨着數次 Minor GC 的執行,將仍然存活的對象移到老年代。

由於絕大部分的對象的生命週期都是非常短的,所以年輕代的 Minor GC 的執行是最頻繁的。

分代回收策略使得大部分回收操作都在堆內存區域的年輕代中進行,而不是整個堆內存,從而使得垃圾回收的效率得到提高。

對象什麼時候進入老年代

我們上面說到對象分配在 Eden 區,隨着數次 Minor GC 的執行,將仍然存活的對象移到老年代,如下圖所示:

在這裏插入圖片描述

那麼有什麼具體的標準表示對象會進入老年代呢?主要有 3 中情況:

  • 大對象(如大數組)直接進入老年代
  • 對象的年齡達到了 MaxTenuringThreshold 設置的閾值,默認爲 15
  • 如果 Survivor 空間中相同年齡的對象大小的總和大於 Survivor 空間的一半,年齡大於等於該年齡的對象就可以直接進入老年代,不用等到對象年齡達到 MaxTenuringThreshold 設置的閾值

分代回收的執行過程

上面我們介紹了爲什麼要分代回收,對象什麼時候進入老年代等。還有一些細節問題沒有說到,比如什麼時候執行 Minor GC,什麼時候對象的年齡加1等等。

下面我們以一組圖文的方式來演示下對象從年輕代到老年代的完整過程(方塊中的數字表示對象的年齡):

  1. 絕大多數的對象都分配在 Eden 區,如下面一個對象將在 Eden 去分配內存:

    流程1

  2. 當 Eden 區被沒有可用空間時,將會觸發 Minor GC:

    流程2

  3. 將 Eden 區中仍然被引用的對象拷貝到第一個 Survivor 空間(S0),清空 Eden 區域時釋放無用對象:

    流程3

  4. 在下一次 Minor GC 時,會執行上面相同的操作:不被引用的對象將會被刪除,存活的對象將會被拷貝到 Survivor 空間,只不過這次是不是拷貝到第一個 Survivor 空間(S0),而是拷貝到第二個 Survivor 空間(S1),此時它們的對象年齡也會加 1。最後 Eden 和 第一個 Survivor 空間都會被清空:

    流程4

  5. 如果又迎來了一次 Minor GC,也會執行相同的操作,此時對象是拷貝到第一個 Survivor 空間(可見每一次 Minor GC 都會切換到另一個 Survivor 空間):

    流程5

  6. 再一次 Minor GC 後,對象的年齡達到閾值後會將對象提升到老年代中(本例子的閾值爲8):

    流程6

  7. 隨着 Minor GC 不斷的執行,不斷的會有對象進入老年代:

    流程7

  8. 以上基本上完整的覆蓋了年輕代的處理過程。最終會在清理壓縮老年代的時候進行 Major GC:

    流程8

什麼時候會觸發 Major GC

通過上面的分析我們知道,當 Eden 區被填滿的時候會觸發 Minor GC。那麼什麼時候會觸發 Major GC 來回收老年代呢?

主要有以下幾個觸發條件:

  1. System.gc()方法的調用

    此方法的調用是建議JVM進行Major GC,雖然只是建議而非一定,但很多情況下它會觸發 Major GC,從而增加 Major GC 的頻率,也即增加了間歇性停頓的次數。強烈建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過通過 -XX:+ DisableExplicitGC 來禁止調用 System.gc。

  2. 老年代空間不足

    老年代空間只有在新生代對象轉入及創建爲大對象、大數組時纔會出現不足的現象,當執行 Major GC 後空間仍然不足,則拋出如下錯誤:java.lang.OutOfMemoryError: Java heap space 爲避免以上兩種狀況引起的 MajorGC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

  3. 方法區空間不足

    JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱爲永生代或者永生區,Permanet Generation 中存放的爲一些 class 的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation 可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Major GC。如果經過 Major GC 仍然回收不了,那麼 JVM 會拋出如下錯誤信息:
    java.lang.OutOfMemoryError: PermGen space
    爲避免 Perm Gen 佔滿造成 Major GC 現象,可採用的方法爲增大 Perm Gen 空間或轉爲使用 CMS GC。

  4. 通過 Minor GC 後進入老年代的平均大小大於老年代的可用內存

    如果發現統計數據之前 Minor GC 的平均晉升大小比目前老年代剩餘的空間大,則不會觸發 Minor GC 而是轉爲觸發 Major GC

  5. 由 Eden 區、From Space區向 To Space 區複製時,對象大小大於 To Space 可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

PermGen VS Metaspace

在 Hotspot 虛擬機中除了年輕代、老年代,還有永久代(PermGen)。

根據 Oracle JVM 官網答疑 對永久代的介紹,永久代主要用於存放:

  • 虛擬機中對 Class 的描述信息,以及 Class 的元數據
  • Class 的靜態數據
  • Interned String

我們在《深入理解 Java 虛擬機(三)~ class 字節碼的執行過程剖析》中提到 方法區 主要用來存放已被虛擬機加載的 class 的結構信息,如運行時常量池、字段和方法數據、方法的代碼。

由此可見,方法區的數據時放在永久代(PermGen)中的。

不同的 JDK 版本對永久代的調整可能對其有調整。根據 Oracle 官方對 JDK1.7 更新描述 可以的得知:
JDK1.7 中不會將 interned strings 放在堆中的永久代中,而是放在主堆中,也就是年輕代和永久代。也就是說會有更多的數據將會在主堆中分配,那麼永久代的數據就變少了。絕大部分的程序不會受到此次修改影響較小,除非是哪些需要加載非常多的類或大量使用 String.intern() 方法的程序。以下是官方的原文:

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.

JDK1.8 中徹底的移除了永久代(PermGen),Class 的元數據信息存放在一個叫 Metaspace 的空間中,內存示意圖如下所示:

metaspace

Metaspace 和 PermGen 的主要區別:

  • 永久代是和 Java Heap 相鄰的(Contiguous with the Java Heap),而 Metaspace 和 Java Heap 不相鄰(Not contiguous with the Java Heap)
  • Metaspace 是在本機內存中分配的
  • Metaspace 的最大可用空間取決於本機系統的可用空間
  • 可用通過 -XX:MaxMetaspaceSize 選項來控制 Metaspace 最大空間

Compressed Class Space

如果啓用了類指針壓縮 UseCompressedClassesPointers 選項,將會有兩個獨立的內存區域分別用來存儲 class 和它的元數據。這兩個獨立的區域分別叫做:MetaspaceCompressed class spaceCompressed class space 邏輯上屬於 Metaspace。如下圖所示:

Compressed Class Space

下面我們來看下 UseCompressedClassesPointers 選項對於對象內存佔用的影響。比如一個空 Object 對象(new Object()),會佔用多大內存?

我們在 深入理解 Java 虛擬機(五)~ 對象的創建過程 中介紹到一個對象的內存佈局爲:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)

下面我們通過編程的方式來精確計算對象的內存佔用:

  • 通過 Instrumentation 類來計算對象大小

    Instrumentation 有一個 getObjectSize 方法可以用來統計對象的大小,Instrumentation 是一個接口,它的實現類對象不需要我們創建,運行 main 之前會自動調用 premain 方法,會將 Instrumentation 對象傳遞進來。

    package gc.objsize;
    import java.lang.instrument.Instrumentation;
    
    public class ObjectSize {
        private static volatile Instrumentation instrumentation;
    
        public static void premain(String args, Instrumentation inst) {
            instrumentation = inst;
        }
    
        public static long getObjectSize(Object obj) {
            if (instrumentation == null)
                throw new IllegalStateException("Instrumentation not initialed");
            return instrumentation.getObjectSize(obj);
        }
    }
    
  • 將 java 類打包成 jar 文件

    1)在 src 目錄下新建一個 MANIFEST.MF 文件:

    Manifest-Version: 1.0
    Premain-Class: gc.objsize.ObjectSize
    Can-Redefine-Classes: true
    

    Premain-Class 指定的就是需要注入的 Instrumentation 對象的類

    2)將相關的 java 文件編譯成 class,並打包成 jar 文件:

    // 編譯 java 文件
    src>javac -encoding UTF-8 gc/objsize/*.java
    
    // 將 class 文件打包成名爲 agent 的 jar 文件
    src>jar -cmf MANIFEST.MF agent.jar gc/objsize/*.class
    
  • 通過 javaagent 注入 Instrumentation 對象

    然後就可以運行我們生成的 ObjectSize 類了:

    src>java -javaagent:agent.jar -cp . gc.objsize.ObjectSize
    

我們測試下面對象的內存佔用情況:

public static void main(String[] args) {
    System.out.println("empty object = " + getObjectSize(new Object()));
    System.out.println("myObject1 = " + getObjectSize(new MyObject1()));
    System.out.println("byte[0] = " + getObjectSize(new byte[0]));
    System.out.println("byte[7] = " + getObjectSize(new byte[7]));
    System.out.println("byte[9] = " + getObjectSize(new byte[9]));
    System.out.println("byte[1024 * 1024] = " + getObjectSize(new byte[1024 * 1024]));
}

public class MyObject1 {
    private Object obj;
}

運行 java -javaagent:agent.jar gc.objsize.ObjectSize 命令的結果如下:

empty object = 16
empty myObject1 = 16
byte[0] = 16
byte[7] = 24
byte[9] = 32
byte[1024 * 1024] = 1048592

由於類指針壓縮 UseCompressedClassesPointers 選項是默認開啓的,我們將該選擇關閉,看下輸出:

// 關閉 UseCompressedClassesPointers
// java -XX:-UseCompressedClassPointers -javaagent:agent.jar gc.objsize.ObjectSize

empty object = 16
empty myObject1 = 24
byte[0] = 24
byte[7] = 32
byte[9] = 40
byte[1024 * 1024] = 1048600

可見類指針壓縮功能,一定程度上減少了內存的佔用。

由此可見一個 Object 對象,在 64bit 的 HotSpot VM 中佔用 16 bytes

上面的 MyObject 中有一個 Object 類型的字段,在 64bit 的 HotSpot VM 中一個 MyObject 在開啓類指針壓縮的情況下佔用 16 個字節,不開啓類壓縮指針佔用 24 個字節。

通過 Instrumentation 可以獲取到對象的佔用大小。通過開啓、關閉 類壓縮指針選項,可以對比出對象內存的不同。但是不能詳細的展示一個對象的哪些部分佔用多少內存,在不開啓類指針壓縮的情況下對象的哪部分內存佔用多了。 這可以使用 JOL (Java Object Layout)來查看對象的內存佈局。

System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
System.out.println(ClassLayout.parseInstance(new MyObject1()));
System.out.println(ClassLayout.parseInstance(new byte[0]).toPrintable());
System.out.println(ClassLayout.parseInstance(new byte[7]).toPrintable());
System.out.println(ClassLayout.parseInstance(new byte[9]).toPrintable());
System.out.println(ClassLayout.parseInstance(new byte[1024 * 1024]).toPrintable());

在 64bit 的 HotSpot VM 中 開啓類壓縮指針 的情況下,各個對象的內存佈局:

# Running 64-bit HotSpot VM.
# Using compressed oop with 0-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION              VALUE
      0     4        (object header)          01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)          00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)          e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

gc.objsize.MyObject1 object internals:
 OFFSET  SIZE               TYPE DESCRIPTION            VALUE
      0     4                    (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)        d4 13 01 20 (11010100 00010011 00000001 00100000) (536941524)
     12     4   java.lang.Object MyObject1.obj          null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION            VALUE
      0     4        (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)        f5 00 00 20 (11110101 00000000 00000000 00100000) (536871157)
     12     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0   byte [B.<elements>          N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION            VALUE
      0     4        (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)        f5 00 00 20 (11110101 00000000 00000000 00100000) (536871157)
     12     4        (object header)        07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     16     7   byte [B.<elements>          N/A
     23     1        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION             VALUE
      0     4        (object header)         01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)         f5 00 00 20 (11110101 00000000 00000000 00100000) (536871157)
     12     4        (object header)         09 00 00 00 (00001001 00000000 00000000 00000000) (9)
     16     9   byte [B.<elements>           N/A
     25     7        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION             VALUE
      0     4        (object header)         01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)         f5 00 00 20 (11110101 00000000 00000000 00100000) (536871157)
     12     4        (object header)         00 00 10 00 (00000000 00000000 00010000 00000000) (1048576)
     16 1048576   byte [B.<elements>         N/A
Instance size: 1048592 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

在 64bit 的 HotSpot VM 中 不開啓類壓縮指針 的情況下,各個對象的內存佈局:

# Running 64-bit HotSpot VM.
# Using compressed oop with 0-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION             VALUE
      0     4        (object header)         01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)         00 1c c9 16 (00000000 00011100 11001001 00010110) (382278656)
     12     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

gc.objsize.MyObject1 object internals:
 OFFSET  SIZE               TYPE DESCRIPTION            VALUE
      0     4                    (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)        c0 b9 36 17 (11000000 10111001 00110110 00010111) (389462464)
     12     4                    (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4   java.lang.Object MyObject1.obj          null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                 VALUE
      0     4        (object header)             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)             a8 07 c9 16 (10101000 00000111 11001001 00010110) (382273448)
     12     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     20     4        (alignment/padding gap)                  
     24     0   byte [B.<elements>               N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                 VALUE
      0     4        (object header)             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)             a8 07 c9 16 (10101000 00000111 11001001 00010110) (382273448)
     12     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)             07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     20     4        (alignment/padding gap)                  
     24     7   byte [B.<elements>               N/A
     31     1        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 1 bytes external = 5 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                 VALUE
      0     4        (object header)             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)             a8 07 c9 16 (10101000 00000111 11001001 00010110) (382273448)
     12     4        (object header)             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)             09 00 00 00 (00001001 00000000 00000000 00000000) (9)
     20     4        (alignment/padding gap)                  
     24     9   byte [B.<elements>               N/A
     33     7        (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 4 bytes internal + 7 bytes external = 11 bytes total

[B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                  VALUE
      0     4        (object header)              01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)              00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)              a8 07 c9 16 (10101000 00000111 11001001 00010110) (382273448)
     12     4        (object header)              00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)              00 00 10 00 (00000000 00000000 00010000 00000000) (1048576)
     20     4        (alignment/padding gap)                  
     24 1048576   byte [B.<elements>              N/A
Instance size: 1048600 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

由此可見,在 64bit 的 Hotspot VM 中雖然一個 Object 對象在開啓和不開啓類壓縮指針的情況下都是佔用 16 個字節,但是他們的內存佈局還是不一樣的。

在開啓類指針壓縮的情況下,一個 Object 對象,它的對象頭(Object header)佔用 12 個字節,內存對齊填充 4 個字節,總共 16 字節。

不開啓類指針壓縮的情況下,一個 Object 對象,它的對象頭(Object header)佔用 16 個字節,剛好是 8 的倍數,所以不需要內存對齊填充,總共 16 字節。

另外數組對象在對象頭中還會存放數組的大小,如 byte[7] 的對象頭中最後的 4 字節就是用來存儲數組的長度的:

 OFFSET  SIZE   TYPE DESCRIPTION            VALUE
      0     4        (object header)        01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)        f5 00 00 20 (11110101 00000000 00000000 00100000) (536871157)
     12     4        (object header)        07 00 00 00 (00000111 00000000 00000000 00000000) (7)

需要注意的是,開啓 UseCompressedClassPointers 的同時需要開啓 UseCompressedOops 選項,否則虛擬機會提示:

Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops

UseCompressedOops 中的 Oop 全稱是 ordinary object pointer (普通對象指針),該選項從 JDK6_u23 版本被默認開啓。

例如對象的屬性指針,數組元素指針都是普通對象指針(Oop)

上面的 MyObject1 類中就有一個 obj 成員屬性,這就是一個普通對象指針

public class MyObject1 {
    private Object obj;
}

在 64 bit 的 Hotspot VM 中,如果開啓 UseCompressedOops,關閉 UseCompressedClassPointers,obj 指針佔用 4 字節:

// 運行時虛擬機參數
-XX:-UseCompressedClassPointers -XX:+UseCompressedOops

gc.objsize.MyObject1 object internals:
 OFFSET  SIZE               TYPE DESCRIPTION           VALUE
      0     4                    (object header)       01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)       88 13 82 17 (10001000 00010011 10000010 00010111) (394400648)
     12     4                    (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4   java.lang.Object MyObject1.obj         null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes

在 64 bit 的 Hotspot VM 中,如果關閉 UseCompressedOops、UseCompressedClassPointers,obj 指針佔用 8 字節:

// 運行時虛擬機參數
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops

gc.objsize.MyObject1 object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           88 d3 44 17 (10001000 11010011 01000100 00010111) (390386568)
     12     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     8   java.lang.Object MyObject1.obj                             null
Instance size: 24 bytes

UseCompressedClassPointers 和 UseCompressedOops 都是默認開啓的。

Code Cache

Code CacheCompressed Class Space 一樣都屬於非堆(NON_HEAP)區域。 Code Cache 也是在本地內存(Native Memory)中分配的。

Code Cache 用於存儲 JIT(Just in Time Compiler) 編譯器生成的代碼。在 Java 中一提到編譯器我們首先想到的可能是 javac 編譯器,它將 Java 文件編譯成 class 文件,以便 JVM 來執行。

但是 class 文件不能被本地機器直接執行,JVM 需要通過解釋器(Interpreter)將 class 文件翻譯層機器能看懂的語言。這種解釋執行的方式性能是比較低的,特別是當某些代碼被執行的頻率比較大的,這種方式就顯得更加低效了。

所以後來 JVM 就加入了 JIT 編譯器,當某塊方法或代碼被執行的次數達到某個閾值時,該段代碼(也稱之爲 Hot Spot Code 熱點代碼)將會被 JIT 編譯器編譯成本地機器碼,然後放入 Code Cache 中存儲,下一次執行時,直接執行機器碼即可。

Code Cache 區域是通過 Code Cache Sweeper 來進行管理的。

關於 Code Cache 需要介紹的的東西還有很多,比如 Code Cache 相關的參數設置,以及實際開發中 Code Cache 的監控與管理,這裏分享兩篇文章:

到此爲止,本文已經介紹了許多關於 JVM 的內存區域,有的是堆區(HEAP),有的是非堆區(NON_HEAP)。在這裏做一個小結:

Memory Pool Name Type
Eden Space
Survivor Space
Old Gen
Metaspace 非堆
Compressed Class Space 非堆
Code Cache 非堆

還可以通過 JVM 工具 jmc 來查看 每個內存區域的使用情況:

JMC

上面的 jmc 展示的內存區域名稱有的在前面加上了 PS,例如 PS Survivor Space。這裏的 PS 指的是收集器的簡稱,PS 的全稱是 Parallel Scavenge。關於收集器後面統一介紹。

理解 OutOfMemoryError 異常

雖然在 JDK 8 中將 PermGen 移除了,新增了 Metaspace。而 Metaspace 是在本機內存中分配的,如果超出了本機內存或者超過了 MaxMetaSpaceSize 設置的值也會拋出 OutOfMemoryError。如果一個對象在堆內存中分配,如果堆中沒有足夠的空間也會拋出 OutOfMemoryError 異常。所以在開發中可能會遇到各種個一樣的 OutOfMemoryError,在這裏我們統一介紹下各式各樣的 OutOfMemoryError。

  • java.lang.OutOfMemoryError: Java heap space

    如果出現該異常,說明 Java 堆中沒有足夠的內存空間爲對象分配。出現這種情況可能有 3 個原因:

    1)堆內存設置的太小

    2)出現了內存泄漏問題,導致對象不能被回收掉

    3)程序中大量使用了 finalize 方法。前面兩個原因我們挺好理解,但是爲什麼大量使用了 finalize 方法可能會導致 OutOfMemoryError 呢?

    我們先來看下 finalize 方法的幾個特點:

    a)從一個對象不可到達開始,到它的 finalize 方法被執行,中間的時間是任意的。也就是說 finalize 方法不能確保被及時的執行,甚至有可能就不會被執行。

    b)爲類提供 finalize 方法可能會延遲對象的回收過程。在 garbage collection 後,覆寫了 finalize 方法的對象將會進入一個隊列中(finalization queue),在 Oracle 和 Sun 實現的虛擬機中會有一個 daemon 線程(finalizer thread)來執行隊列中對象的 finalize 方法。我們知道 daemon 線程的優先級最低,如果此時開發者在應用中創建了一個高優先級的線程,可能導致 finalization queue 的增長速度快於 finalizer thread 的釋放速度,那麼 Java 堆可能會被填滿,然後拋出 OutOfMemoryError 異常。

    所以在實際開發中最好不要使用 finalize 方法,釋放相關資源可以顯示的調用自定義的釋放方法。

    雖然 finalize 方法可能會導致各種問題,但是也不是說它一無是處。在 Java 源碼中也不乏 finalize 的影子的,例如 FileInputStream、FileOutputStream、Timer、Collection 等,Java 的設計者都爲其提供了 finalize 方法,當開發者沒有調用 “close” 方法,那麼 finalize 方法就充當了 “安全網(safety net)” ,也就是最後一道防線,因爲資源晚點釋放總比不釋放要好。例如 FileOutputStream 的 finalize 方法:

    protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {
                close();
            }
        }
    }
    
  • java.lang.OutOfMemoryError: GC Overhead limit exceeded

    如果出現該異常,說明垃圾收集器(Garbage Collector)一直在運行,並且 Java 程序運行的非常慢。在一次垃圾收集(Garbage Collection)後,如果 Java 進程花費了大約 98% 的時間來進行垃圾收集,並且它只恢復了不到 2% 的堆內存,並且到目前爲止進行了 5 次連續的垃圾收集,將會拋出 OutOfMemoryError

    如果不想拋出可以加大堆內存,或者關閉 -XX:-UseGCOverheadLimit 選項。

  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit

    如果出現該異常,說明程序試圖分配一個大於堆內存的數組。例如 堆最大值爲 256M,程序試圖分配 512M 的數組,將會拋出該異常。

  • java.lang.OutOfMemoryError: Metaspace

    我們知道,class元數據(虛擬機內存)都會存放在 Metaspace 中。如果程序中需要加載的 class 非常多,Metaspace 超過了 MaxMetaSpaceSize 設置的值會拋出 OutOfMemoryError。如果不設置 MaxMetaSpaceSize,當物理內存不足,有可能會引起內存交換(swapping),嚴重拖累系統性能。

  • java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space?

    本地內存分配失敗。一個應用的 Java Native Interface(JNI) 代碼、本地庫及Java 虛擬機都從本地堆分配內存分配空間。當從本地堆分配內存失敗時拋出 OutOfMemoryError 異常。例如:當物理內存及交換分區都用完後,再次嘗試從本地分配內存時也會拋出該異常。

  • java.lang.OutOfMemoryError: Compressed class space

    通過上面的介紹的我們知道,Compressed class space 邏輯上屬於 Metaspace 的一部分。當我們開啓 UseCompressedClassPointer 選項,那麼 class 元數據將放在 Compressed class space 中,它的大小默認爲 1G,可以通過 CompressedClassSpaceSize 來設置其大小。如果程序需要加載的類很多,超過了 CompressedClassSpaceSize 的限制,則會拋出該異常。

  • java.lang.OutOfMemoryError: reason stack_trace_with_native_method

    如果該異常的堆棧信息被打印出來,其中第一幀是 Native Method,則表明 Native Method 遇到了內存分配故障。如果拋出此類 OutOfMemoryError 異常,則可能需要使用操作系統的相關工具序來進一步診斷問題(Native Operating System Tools)。

JVM 垃圾收集器

上面我們介紹完了常用的收集算法和 JVM 中關於垃圾回收的內存區域,我們就可以來介紹 JVM 中內置的一些垃圾收集器(Garbage Collector)了,垃圾回收的工作正是垃圾收集器來完成的。

在介紹垃圾收集器之前,我們需要明白一些與之相關的術語:

  • Stop the world

    當垃圾回收期在執行回收的時候,應用程序的所有線程被暫停

  • Parallel

    Parallel(並行)指兩個或多個事件在同一時刻發生,在現代計算機中通常指多臺處理器上同時處理多個任務

    上面是對並行的傳統定義,是從處理器角度出發的,但是在 JVM 垃圾回收器的並行不是從處理器角度出發的,這裏的並行是指多個垃圾回收線程在操作系統上並行運行,這裏強調的是垃圾回收線程。Java 應用程序暫停執行(Stop the world)。

  • Concurrent

    Concurrent(併發)指兩個或多個事件在同一時間間隔內發生,在現代計算機中一臺處理器 “同時” 處理多個任務,那麼這個任務只能交替運行,從處理器的角度上看只能串行執行,從用戶的角度看這些任務是 “並行” 執行。

    上面是對併發的傳統定義,是從處理器角度出發的,但是在 JVM 垃圾回收器的併發並不是從處理器角度出發,指的是垃圾回收的線程併發運行,同時這些線程和 Java 應用程序併發運行。

  • Incremental

    垃圾回收器對堆的某部分(增量)進行回收而不是掃描整個堆。

  • Throughput

    吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即 吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。假設虛擬機總共運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%

接下來,我們將會介紹 7 種常見的垃圾收集器。由於每個垃圾收集器的特點不同,它們回收的堆內存區域也不同,有的收集器是是針對年輕代的,有的是針對老年代的。下面通過一張圖來概要性的描述垃圾收集器是作用在哪個代(Generation)的 ,哪些收集器是可以進行組合工作的:

JVM-Collectors

實線連接的表示可以進行組合收集,間隔虛線連接的表示在 Java 9 中不能組合。左下角虛線表示當 CMS 發生 CMS Concurrent mode failure 時可以使用 Serial Old 作爲 CMS 的備用方案。

Serial 收集器

Serial(串行)收集器使用單線程進行垃圾回收,在回收的時候需要暫停其他的工作線程,新生代通常採用複製算法,老年代通常採用標記(標記清理、 標記整理)算法。Serial 收集器的線程交互圖如下圖所示:

Serial 收集器

可以通過 -XX:+UseSerialGC 來告訴虛擬機使用 Serial 收集器

如果應用程序的數據集比較小(小於100M),可以使用 Serial 收集器

如果應用程序將在單個處理器上運行,並且不需要暫停時間,那麼讓虛擬機選擇收集器,或者指定使用 Serial 收集器

ParNew 收集器

ParNew 收集器就是 Serial 收集器的多線程版本。除了使用多線程進行垃圾收集外,其餘行爲包括 Serial 收集器可用的所有控制參數、收集算法(複製算法)、Stop The World、對象分配規則、回收策略等與 Serial 收集器完全相同,兩者共用了相當多的代碼。ParNew 收集器的線程交互圖如下圖所示:

ParNew 收集器

可以通過 -XX:+UseParNewGC 來告訴虛擬機使用 ParNew 收集器。它默認開啓的收集線程數與 CPU 的數量相同,也可以通過 -XX:ParallerGCThreads 來設置 GC 線程數量

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一個並行的多線程新生代收集器。Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS 等收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標是達到一個可控制的 吞吐量(Throughput)

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗。

而高吞吐量則可以高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

可以通過 -XX:+UseParallelGC 來啓用 Parallel Scavenge 收集器

可以通過 -XX:MaxGCPauseMillis 來設置吞吐量,也可以直接設置吞吐量 -XX:GCTimeRatio,值爲 0 ~ 100

還可以打開 -XX:+UseAdaptiveSizePolicy 開關,這樣就不需要手動指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種方式稱爲 GC 自適應的調節策略(GC Ergonomics)。自適應調節策略也是 Parallel Scavenge 收集器與 ParNew 收集器的一個重要區別。

Serial Old 收集器

Serial Old 收集器是 Serial 的老年代版本,它同樣是單線程的收集器,使用了 “標記清理” 算法。

主要用於:

  • 給 Client 模式下的虛擬機使用
  • 如果是 Server 模式,在 JDK 1.5 之前的版本與 Parallel Scavenge 搭配使用
  • 如果是 Server 模式,在 CMS 收集器發生 Concurrent Mode Failure 時,作爲 CMS 的備用方案

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它非常符合那些集中在互聯網站或者B/S系統的服務端上的Java應用,這些應用都非常重視服務的響應速度。從名字上就可以看出它是基於 “標記-清除” 算法實現的。

CMS 的運作過程相對前面幾種收集器要複雜一些,整體步驟分爲 4 個步驟:

  • 初始標記(Initial Mark)

    僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要 “Stop The World”。

  • 併發標記

    進行 GC Roots Tracing 的過程,在整個過程中耗時最長。

  • 重新標記

    爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。此階段也需要 “Stop The World”。

  • 併發清除

    在標記階段收集標識爲不可到達的對象。死對象的集合將該對象的空間添加到空閒列表中,供以後分配。此時可能會發生死對象的合併。注意,活動對象不會被移動。

CMS 收集器的運行示意圖:

CMS 收集器的運行示意圖

通過 -XX:+UseConcMarkSweepGC 參數,啓用 CMS 收集器

CMS 是一款優秀的收集器:併發收集、低延遲。Sun 公司的官方文檔上也稱之爲併發低停頓收集器(Concurrent Low Pause Collector),雖然 CMS 很優秀,但是也有 3 個明顯的缺點:

  • CMS 收集器對 CPU 資源非常敏感

    1) 因爲是併發收集所以會佔用一部分線程(CPU資源),雖然不會導致用戶線程暫停,但是會導致應用程序變慢,總吞吐量降低

    2) 默認情況下,開啓的 線程數 爲(CPU 的數量 + 3)/ 4,當 CPU 數量少於 4 個時,併發回收時垃圾收集線程不少於 25% 的 CPU 資源,並且隨着 CPU 數量的增加而下降;當 CPU 不足 4 個時(比如2個),CMS 對用戶程序的影響就可能變得很大,如果本來 CPU 負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了 50%。

    3) 爲了應付上面的情況,虛擬機提供了一種 “增量式併發收集器” (i-cms),就是在併發階段減少對應用程序的影響,減少對 CPU 資源的佔用。在 Java 8 中該模式已經被廢棄,具體信息可以查看 Oracle CMS

  • CMS 收集器無法處理浮動垃圾,可能出現 “Concurrent Mode Failure” 失敗而導致另一次 Full GC 的產生

    1) 由於 CMS 併發清理階段用戶線程還在運行,可能會產生新的垃圾,這一部分垃圾出現在標記過程之後,CMS 無法在檔次收集中處理掉它們,這部分垃圾稱之爲 “浮動垃圾”

    2) 就是以爲垃圾收集階段用戶線程還在運行,也就是說需要預留足夠的空間給用戶線程使用。所以 CMS 收集器不能等到老年代空間幾乎使用完畢在進行回收,需要預留一部分空間提供給併發收集時應用程序運作使用

    3) JDK 1.5 默認設置下,CMS 收集器在老年代使用了 68% 的空間後被激活。可以通過 XX:CMSInitiatingOccupancyFraction 自定義閾值(0-100)

    4) JDK 1.6 中,CMS 收集器的啓動閾值提高到了 92%。

    5) 如果 CMS 運行期間預留內存無法滿足程序需要,就會出現 “Concurrent Mode Failure” 失敗,虛擬機會臨時啓用 Serial Old 收集器來重新進行老年代的垃圾收集。這樣會導致停頓時間就很長。所以 XX:CMSInitiatingOccupancyFraction 設置的太高可能導致大量的 “Concurrent Mode Failure” 失敗,程序性能反而降低。

  • 由於 CMS 使用標記清除算法,所以會產生大量的空間碎片

    1) 當老年代空間碎片過多時,就算可用空間大,分配對象時如果找不到足夠大的連續空間將不得不提前觸發一次 Full GC,所以 CMS 收集器提供了 -XX:+UseCMSCompactAtFullCollection 開關參數,默認爲打開狀態,用於在 CMS 頂不住要進行 Full GC 時開啓內存碎片合併整理過程,內存整理的過程無法併發的,那麼停頓時間會變長。

    2) CMS 還提供了另一個參數 -XX:CMSFullGCsBeforeCompaction,用於設置每執行多少次不壓縮的 Full GC ,執行一次帶壓縮整理。

更多關於 CMS 收集器的信息,可以參考 官網 對 CMS 的描述。

Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用 “標記-整理” 算法。

該收集器於 JDK 1.6 版本開始提供,在此之前新生代的 Parallel Scavenge 只能和 Serial Old 進行搭配使用,但是 Serial Old 收集器在服務器端應用性能上表現不好。直到 Parallel Old 收集器出現或,“吞吐量有限” 收集器纔有比較名副其實的應用組合,在注重吞吐量和 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 和 Parallel Old 收集器。

Parallel Scavenge 和 Parallel Old 收集器的運行示意圖:

Parallel Scavenge/Parallel Old

G1 收集器

G1(Garbage-First)是一款面向服務端(Server-Style)應用的垃圾收集器,用於多核處理器和大內存的機器上。實現高吞吐量的情況下,儘可能的降低暫停時間(pause time)。G1 收集器在 JDK7 update 4 版本上得到完全的支持,主要是爲以下應用而設計的一款收集器:

  • 像 CMS 收集器一樣,能夠與應用線程一起併發操作
  • 不會因爲整理(Compact)堆內存而導致 Pause time
  • 需要更多可預測的 GC 暫停時間
  • 不想犧牲太多的吞吐量性能
  • 不需要更大的 Java 堆

與上面介紹的 GC 收集器相比,G1 具備如下特點:

  • 並行與併發。G1 能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短 “Stop The World” 停頓時間,部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過併發的方式讓 Java 程序繼續執行。

  • 分代收集。與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但它能夠採用不同方式去處理新創建的對象和已存活一段時間、熬過多次 GC 的舊對象來獲取更好的收集效果。

  • 空間整合。G1從整體來看是基於 “標記-整理” 算法實現的收集器,從局部(兩個Region之間)上來看是基於 “複製” 算法實現的。這意味着 G1 運行期間不會產生內存空間碎片,收集後能提供規整的可用內存。此特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次 GC。

  • 可預測的停頓。這是 G1 相對 CMS 的一大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了降低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在GC上的時間不得超過 N 毫秒。

Heap Region

雖然在 G1 收集器中保留了分代的概念,但是它不要求堆內存是連續的,G1 將堆拆分成一系列的分區(Heap Region),這樣在一段時間內,大部分的垃圾回收操作只只針對一部分區域,而不是整個堆。

G1 的分區也稱堆分區,是 G1 堆和操作系統交互的最小管理單位。G1 的分區類型(HeapRegion Type)大致可以分爲四類:

  • 自由分區(Free Heap Region,FHR)
  • 新生代分區(Young Heap Region,YHR)
  • 大對象分區(Humongous Heap Region,HHR)
  • 老生代分區(Old Heap Region,OHR)

G1 堆佈局如下圖所示:

G1 堆佈局

Heap Region 的大小隨虛擬機啓動被確定,可以通過參數 -XX:G1HeapRegionSize 來指定,它的範圍是: 1M ~ 32M。如果不指定 Heap Region Size 虛擬機會根據 Heap 大小啓發推斷出它的大小

G1 的最佳實踐

不要設置年輕代大小

顯式地通過 -Xmn 設置年輕代的大小會干預了 G1 收集器的默認行爲。

  • G1 將不再考慮垃圾收集的暫停時間的目標。因此,從本質上講,設置年輕一代的規模會阻礙暫停時間目標的實現。
  • G1 不再能夠根據需要擴展和收縮年輕一代的空間。因爲大小是固定的,所以不能改變大小。

響應時間指標

不要使用平均響應時間(ART)作爲設置 XX:MaxGCPauseMillis=<N> 的度量,G1 可能只會滿足你目標值 90% 或者花費更多的時間。這意味着 90% 發出請求的用戶的響應時間不會高於目標值。暫停時間是一個目標,G1 並不能保證總是能達到。

避免轉移失敗(Evacuation Failure)

當 JVM 收集 Survivor 和 對象晉級(Promote)時虛擬機用盡了 Heap Region 內存,將會發生 promotion failure。因爲堆內存已經達到了最大值,不能擴展。-XX:+PrintGCDetails 輸出的日誌 to-space 將會體現這個錯誤。出現這個錯誤會拖累程序的性能:

  • GC 仍然需要繼續,所以必須釋放空間。
  • 未成功複製的對象必須保留在適當的位置。
  • CSet 中區域的 RSets 的任何更新都必須重新生成
  • 所有的這些步驟代價都是昂貴,所以儘量避免 Evacuation Failure

G1 收集器是非常複雜的,更多關於 G1 收集器相關的知識,大家可以查閱相關的資料和書籍,這裏列舉一些官方的文檔:

Java 7, Java 8, Java 9 垃圾回收的變化

JDK 7 Changes

JDK 7 開始移除了部分 PermGen:

  • 符號引用(Symbols)從 PermGen 移到了 Native Memory
  • Interned String 從 PermGen 移到了 Heap Memory
  • Class 靜態數據從 PermGen 移到了 Java Heap
  • 可能會略微增加年輕代垃圾收集的暫停時間
  • 你可以使用 –XX:+JavaObjectsInPerm 撤銷上面的改變

JDK 8 Changes

徹底移除了 PermGen:

  • JDK 8 中徹底移除了 PermGen
  • JDK 8 中 PermGen 相關的調優參數都被廢棄

JDK 8 中的 Metaspace:

  • 從 JDK 8 開始假如 Metaspace
  • Class 和 元數據存儲在 Metaspace 中
  • Metaspace 用來代替 PermGen
  • Metaspace 增加了調優參數:MetaspaceSize、MaxMetaspaceSize

JDK8 關於類卸載相關的變化:

  • 直到 JDK 8u40,G1 的只會在 Full GC 的時候進行類卸載
  • 從 JDK8 開始 G1 收集器支持併發進行類卸載

JDK 9 Changes

JDK 9 關於 G1 和 CMS:

  • JDK 9 將 G1 收集器作爲默認收集器
  • JDK 9 廢棄了 CMS 收集器,計劃在未來的 major release 版本中移除 CMS

JDK 中下列的 GC 組合被移除:

  1. DefNew + CMS : -XX:-UseParNewGC -XX:+UseConcMarkSweepGC
  2. ParNew + SerialOld : -XX:+UseParNewGC
  3. ParNew + iCMS : -Xincgc
  4. ParNew + iCMS : -XX:+CMSIncrementalMode -XX:+UseConcMarkSweepGC
  5. DefNew + iCMS : -XX:+CMSIncrementalMode -XX:+UseConcMarkSweepGC -XX:-UseParNewGC
  6. CMS foreground : -XX:+UseCMSCompactAtFullCollection
  7. CMS foreground : -XX:+CMSFullGCsBeforeCompaction
  8. CMS foreground : -XX:+UseCMSCollectionPassing

DefNew 是 Default New Generation 的簡稱,關於 DefNew 的由來可以參考:RednaxelaFX的回答。CMS GC 在實現上分成 foreground collector 和 background collector。foreground collector 相對比較簡單,background collector 比較複雜,關於參考:JVM 源碼解讀之 CMS GC 觸發條件

Reference

官方文檔博客視頻資料:

關於 RednaxelaFX 資料:

相關書籍:

  • 《JVM G1 源碼分析和調優》
  • 《Effective Java(第2版)》

如果你覺得本文幫助到你,給我個關注和讚唄!

另外本文涉及到的代碼都在我的 AndroidAll GitHub 倉庫中。該倉庫除了 Java虛擬機 技術,還有 Android 程序員需要掌握的技術棧,如:程序架構、設計模式、性能優化、數據結構算法、Kotlin、Flutter、NDK,以及常用開源框架 Router、RxJava、Glide、LeakCanary、Dagger2、Retrofit、OkHttp、ButterKnife、Router 的原理分析 等,持續更新,歡迎 star。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章