jvm學習筆記

2023-07-01 12:00:08

1. JVM快速入門

從面試開始:

  1. 請談談你對JVM 的理解?java8 的虛擬機器器有什麼更新?

  2. 什麼是OOM ?什麼是StackOverflowError?有哪些方法分析?

  3. JVM 的常用引數調優你知道哪些?

  4. 記憶體快照抓取和MAT分析DUMP檔案知道嗎?

  5. 談談JVM中,對類載入器你的認識?

​ 位置:JVM是執行在作業系統之上的,它與硬體沒有直接的互動

1.1. 結構圖

方法區:儲存已被虛擬機器器載入的類後設資料資訊(元空間)

堆:存放物件範例,幾乎所有的物件範例都在這裡分配記憶體

虛擬機器器棧:虛擬機器器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊

程式計數器:當前執行緒所執行的位元組碼的行號指示器

本地方法棧:本地方法棧則是為虛擬機器器使用到的Native方法服務

1.2. 類載入器ClassLoader

負責載入class檔案,class檔案在檔案開頭有特定的檔案標示,並且ClassLoader只負責class檔案的載入,至於它是否可以執行,則由Execution Engine決定。

類載入器分為四種:前三種為虛擬機器器自帶的載入器。

  • 啟動類載入器(Bootstrap)C++

    負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類

  • 擴充套件類載入器(Extension)Java

    負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

  • 應用程式類載入器(AppClassLoader)Java

    也叫系統類載入器,負責載入classpath中指定的jar包及目錄中class

  • 使用者自定義載入器 Java.lang.ClassLoader的子類,使用者可以客製化類的載入方式

工作過程:

  • 1、當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類別載入器ExtClassLoader去完成。
  • 2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  • 4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入
  • 5、如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException

其實這就是所謂的雙親委派模型。簡單來說:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上

好處:防止記憶體中出現多份同樣的位元組碼(安全性角度)
比如載入位於 rt.jar 包中的類 java.lang.Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個 Object物件。

寫段兒程式碼演示類載入器:

public class Demo {

    public Demo() {
        super();
    }

    public static void main(String[] args) {
        Object obj = new Object();
        String s = new String();
        Demo demo = new Demo();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(s.getClass().getClassLoader());
        System.out.println(demo.getClass().getClassLoader().getParent().getParent());
        System.out.println(demo.getClass().getClassLoader().getParent());
        System.out.println(demo.getClass().getClassLoader());
    }
}

列印控制檯中的sun.misc.Launcher,是一個java虛擬機器器的入口應用

1.3. 執行引擎Execution Engine

Execution Engine執行引擎負責解釋命令,提交作業系統執行。

1.4. 本地介面Native Interface

​ 本地介面的作用是融合不同的程式語言為 Java 所用,它的初衷是融合 C/C++程式,Java 誕生的時候是 C/C++橫行的時候,要想立足,必須有呼叫 C/C++程式,於是就在記憶體中專門開闢了一塊區域處理標記為native的程式碼,它的具體做法是 Native Method Stack中登記 native方法,在Execution Engine 執行時載入native libraies。

​ 目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見。因為現在的異構領域間的通訊很發達,比如可以使用 Socket通訊,也可以使用Web Service等等,不多做介紹。

1.5. Native Method Stack

它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執行時載入本地方法庫。

1.6. PC暫存器

每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,指向方法區中的方法位元組碼(用來儲存指向下一條指令的地址,即 將要執行的指令程式碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。

1.7. Method Area方法區

方法區是被所有執行緒共用,所有欄位和方法位元組碼,以及一些特殊方法如建構函式,介面程式碼也在此定義。簡單說,所有定義的方法的資訊都儲存在該區域,此區屬於共用區間

靜態變數+常數+類資訊(構造方法/介面定義)+執行時常數池存在方法區中

But

範例變數存在堆記憶體中,和方法區無關

2. stack棧

Stack 棧是什麼?

​ 棧也叫棧記憶體,主管Java程式的執行,是線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束該棧就Over,生命週期和執行緒一致,是執行緒私有的。8種基本型別的變數+物件的參照變數+實體方法都是在函數的棧記憶體中分配。

棧儲存什麼?

棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和執行期資料的資料集。

棧幀中主要儲存3 類資料:

  • 本地變數(Local Variables):輸入引數和輸出引數以及方法內的變數。

  • 棧操作(Operand Stack):記錄出棧、入棧的操作。

  • 棧幀資料(Frame Data):包括類檔案、方法等等。

棧執行原理:

當一個方法A被呼叫時就產生了一個棧幀 F1,並被壓入到棧中,

A方法又呼叫了 B方法,於是產生棧幀 F2 也被壓入棧,

B方法又呼叫了 C方法,於是產生棧幀 F3 也被壓入棧,

……

執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……

遵循「先進後出」或者「後進先出」原則。

圖示在一個棧中有兩個棧幀:

棧幀 2是最先被呼叫的方法,先入棧,

然後方法 2 又呼叫了方法1,棧幀 1處於棧頂的位置,

棧幀 2 處於棧底,執行完畢後,依次彈出棧幀 1和棧幀 2,

執行緒結束,棧釋放。

每執行一個方法都會產生一個棧幀,儲存到棧(後進先出)的頂部,頂部棧就是當前的方法,該方法執行完畢
後會自動將此棧幀出棧。

常見問題棧溢位:Exception in thread "main" java.lang.StackOverflowError

通常出現在遞迴呼叫時。

3. 堆

堆疊方法區的關係:

HotSpot是使用指標的方式來存取物件:

  • Java堆中會存放存取類後設資料的地址

  • reference儲存的就是物件的地址

三種JVM:

•Sun公司的HotSpot

•BEA公司的JRockit

•IBM公司的J9 VM

3.1. 堆體系概述

Java7之前

Heap 堆:一個JVM範例只存在一個堆記憶體,堆記憶體的大小是可以調節的。類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,儲存所有參照型別的真實資訊,以方便執行器執行,堆記憶體邏輯上分為三部分:

  • Young Generation Space 新生區 Young/New

  • Tenure generation space 養老區 Old/Tenure

  • Permanent Space 永久區 Perm

也稱為:新生代(年輕代)、老年代、永久代(持久代)。

3.1.1. 新生區

​ 新生區是物件的誕生、成長、消亡的區域,一個物件在這裡產生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分: 伊甸區(Eden space)和倖存者區(Survivor pace) ,所有的物件都是在伊甸區被new出來的。倖存區有兩個: From區(Survivor From space)和To區(Survivor To space)。當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他物件所參照的物件進行銷燬。然後將伊甸園中的剩餘物件移動到倖存 From區。

MinorGC垃圾回收的過程如下:

  1. eden、From 複製到 To,年齡+1
    首先,當Eden區滿的時候會觸發第一次GC,把還活著的物件拷貝到Survivor From區,當Eden區再次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域(如果有物件的年齡已經達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1

  2. 清空 eden、Survivor From
    然後,清空Eden和From中的物件

  3. To和 From 互換
    最後,To和From互換,原To成為下一次GC時的From區。部分物件會在From和To區域中複製來複制去,如此交換15次(由JVM引數MaxTenuringThreshold決定,這個引數預設是15),最終如果還是存活,就存入到老年代

  4. 大物件特殊情況
    如果分配的新物件比較大Eden區放不下,但Old區可以放下時,物件會被直接分配到Old區(即沒有晉升這一過程,直接到老年代了)

MinorGC的過程:複製 -> 清空 -> 互換

3.1.2. 老年代

經歷多次GC仍然存在的物件(預設是15次),老年代的物件比較穩定,不會頻繁的GC

若養老區也滿了,那麼這個時候將產生MajorGC(FullGC),進行養老區的記憶體清理。若養老區執行了Full GC之後發現依然無法進行物件的儲存,就會產生OOM異常「OutOfMemoryError」。

如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機器器的堆記憶體不夠。原因有二:

(1)Java虛擬機器器的堆記憶體設定不夠,可以通過引數-Xms、-Xmx來調整。

(2)程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被參照)。

3.1.3. 永久代

​ 永久儲存區是一個常駐記憶體區域,用於存放JDK自身所攜帶的 Class、Interface 的後設資料,也就是說它儲存的是執行環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所佔用的記憶體。

​ 對於HotSpot虛擬機器器,很多開發者習慣將方法區稱之為「永久代(Parmanent Gen)」 ,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區(相當於是一個介面interface)的一個實現。

​ 實際而言,方法區(Method Area)和堆一樣,是各個執行緒共用的記憶體區域,它用於儲存虛擬機器器載入的:類資訊+普通常數+靜態常數+編譯器編譯後的程式碼等等,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

​ 如果出現java.lang.OutOfMemoryError: PermGen space說明是Java虛擬機器器對永久代Perm記憶體設定不夠。一般出現這種情況,都是程式啟動需要載入大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被載入,最終導致Perm區被佔滿。

Jdk1.6及之前: 有永久代,常數池1.6在方法區

Jdk1.7: 有永久代,但已經逐步「去永久代」,常數池1.7在堆

Jdk1.8及之後: 無永久代,常數池1.8在堆中

永久代與元空間的最大區別之處:

永久代使用的是jvm的堆記憶體,但是java8以後的元空間並不在虛擬機器器中而是使用本機實體記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。

3.2. 堆引數調優入門

均以JDK1.8+HotSpot為例

jdk1.7:

jdk1.8:

3.2.1. 常用JVM引數

怎麼對jvm進行調優?通過引數設定

引數 備註
-Xms 初始堆大小。只要啟動,就佔用的堆大小,預設是記憶體的1/64
-Xmx 最大堆大小。預設是記憶體的1/4
-Xmn 新生區堆大小
-XX:+PrintGCDetails 輸出詳細的GC處理紀錄檔

java程式碼檢視jvm堆的預設值大小:

Runtime.getRuntime().maxMemory()   // 堆的最大值,預設是記憶體的1/4
Runtime.getRuntime().totalMemory()  // 堆的當前總大小,預設是記憶體的1/64

3.2.2. 怎麼設定JVM引數

程式執行時,可以給該程式設定jvm引數,不同的工具設定方式不同。

如果是命令列執行:

java -Xmx50m -Xms10m HeapDemo

eclipse執行的設定方式如下:

idea執行時設定方式如下:

3.2.3. 檢視堆記憶體詳情

public class Demo2 {
    public static void main(String[] args) {

        System.out.print("最大堆大小:");
        System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
        System.out.print("當前堆大小:");
        System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
        System.out.println("==================================================");

        byte[] b = null;
        for (int i = 0; i < 10; i++) {
            b = new byte[1 * 1024 * 1024];
        }
    }
}

執行前設定引數:-Xmx50m -Xms30m -XX:+PrintGCDetails

執行:看到如下資訊

新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory()

3.2.4. GC演示

public class HeapDemo {

    public static void main(String args[]) {

        System.out.println("=====================Begin=========================");
        System.out.print("最大堆大小:Xmx=");
        System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");

        System.out.print("剩餘堆大小:free mem=");
        System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");

        System.out.print("當前堆大小:total mem=");
        System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

        System.out.println("==================First Allocated===================");
        byte[] b1 = new byte[5 * 1024 * 1024];
        System.out.println("5MB array allocated");

        System.out.print("剩餘堆大小:free mem=");
        System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");

        System.out.print("當前堆大小:total mem=");
        System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

        System.out.println("=================Second Allocated===================");
        byte[] b2 = new byte[10 * 1024 * 1024];
        System.out.println("10MB array allocated");

        System.out.print("剩餘堆大小:free mem=");
        System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");

        System.out.print("當前堆大小:total mem=");
        System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

        System.out.println("=====================OOM=========================");
        System.out.println("OOM!!!");
        System.gc();
        byte[] b3 = new byte[40 * 1024 * 1024];
    }
}

jvm引數設定成最大堆記憶體100M,當前堆記憶體10M:-Xmx100m -Xms10m -XX:+PrintGCDetails

再次執行,可以看到minor GC和full GC紀錄檔:

3.2.5. OOM演示

把上面案例中的jvm引數改成最大堆記憶體設定成50M,當前堆記憶體設定成10M,執行測試: -Xmx50m -Xms10m

=====================Begin=========================
 	剩餘堆大小:free mem=8.186859130859375M
當前堆大小:total mem=9.5M
=================First Allocated=====================
5MB array allocated
剩餘堆大小:free mem=3.1868438720703125M
當前堆大小:total mem=9.5M
================Second Allocated====================
10MB array allocated
剩餘堆大小:free mem=3.68682861328125M
當前堆大小:total mem=20.0M
=====================OOM=========================
OOM!!!
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.atguigu.demo.HeapDemo.main(HeapDemo.java:40)

實際開發中怎麼定位這種錯誤資訊?MAT工具

3.3. MAT工具

安裝方式:eclipse外掛市場下載

3.3.1. MAT工具的使用

執行引數:-Xmx30m -Xms10m -XX:+HeapDumpOnOutOfMemoryError

重新重新整理專案:看到dump檔案

開啟:

3.3.2. idea分析dump檔案

把上例中執行引數改成:

-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp 

-XX:HeapDumpPath:生成dump檔案路徑。

再次執行:生成C:\tmp\java_pid20328.hprof檔案

生成的這個檔案怎麼開啟?jdk自帶了該型別檔案的解讀工具:jvisualvm.exe

雙擊開啟:

檔案-->裝入-->選擇要開啟的檔案即可

裝入後:

3.4. 常用命令列(瞭解)

檢視java程序:jps -l

檢視某個java程序所有引數:jinfo 程序號

檢視某個java程序總結性垃圾回收統計:jstat -gc 20292

3.5. jvm結構總結

4. GC垃圾回收

面試題:

  • JVM記憶體模型以及分割區,需要詳細到每個區放什麼
  • 堆裡面的分割區:Eden,survival from to,老年代,各自的特點。
  • GC的三種收集方法:標記清除、標記整理、複製演演算法的原理與特點,分別用在什麼地方
  • Minor GC與Full GC分別在什麼時候發生

JVM垃圾判定演演算法:(物件已死?)

  • 參照計數法(Reference-Counting)
  • 可達性分析演演算法(根搜尋演演算法)

GC垃圾回收主要有四大演演算法:(怎麼找到已死物件並清除?)

  • 複製演演算法(Copying)
  • 標記清除(Mark-Sweep)
  • 標記壓縮(Mark-Compact),又稱標記整理
  • 分代收集演演算法(Generational-Collection)

4.1. JVM複習

JVM結構圖:

堆記憶體結構:

GC的特點:

  • 次數上頻繁收集Young區
  • 次數上較少收集Old區
  • 基本不動Perm區

4.2. 垃圾判定

4.2.1. 參照計數法(Reference-Counting)

參照計數演演算法是通過判斷物件的參照數量來決定物件是否可以被回收。

給物件中新增一個參照計數器,每當有一個地方參照它時,計數器值就加1;當參照失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。

優點:

  • 簡單,高效,現在的objective-c、python等用的就是這種演演算法。

缺點:

  • 參照和去參照伴隨著加減演演算法,影響效能

  • 很難處理迴圈參照,相互參照的兩個物件則無法釋放。

因此目前主流的Java虛擬機器器都摒棄掉了這種演演算法

4.2.2. 可達性分析演演算法

這個演演算法的基本思想就是通過一系列的稱為 「GC Roots」 的物件作為起點,從這些節點開始向下搜尋,節點所走過的路徑稱為參照鏈,當一個物件到 GC Roots 沒有任何參照鏈相連的話,則證明此物件是不可用的。

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

  • 虛擬機器器棧(棧幀中的本地變數表)中的參照物件。
  • 方法區中的類靜態屬性參照的物件。
  • 方法區中的常數參照的物件。
  • 本地方法棧中JNI(Native方法)的參照物件

真正標記以為物件為可回收狀態至少要標記兩次。

第一次標記:不在 GC Roots 鏈中,標記為可回收物件。

第二次標記:判斷當前物件是否實現了finalize() 方法,如果沒有實現則直接判定這個物件可以回收,如果實現了就會先放入一個佇列中。並由虛擬機器器建立一個低優先順序的程式去執行它,隨後就會進行第二次小規模標記,在這次被標記的物件就會真正被回收了!

4.2.3. 四種參照

平時只會用到強參照和軟參照。

強參照:

​ 類似於 Object obj = new Object(); 只要強參照還存在,垃圾收集器永遠不會回收掉被參照的物件。

軟參照:

​ SoftReference 類實現軟參照。在系統要發生記憶體溢位異常之前,才會將這些物件列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。軟參照可用來實現記憶體敏感的快取記憶體。

弱參照:

​ WeakReference 類實現弱參照。物件只能生存到下一次垃圾收集之前。在垃圾收集器工作時,無論記憶體是否足夠都會回收掉只被弱參照關聯的物件。

虛參照:

​ PhantomReference 類實現虛參照。無法通過虛參照獲取一個物件的範例,為一個物件設定虛參照關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

4.3. 垃圾回收演演算法

在介紹JVM垃圾回收演演算法前,先介紹一個概念:Stop-the-World

Stop-the-world意味著 JVM由於要執行GC而停止了應用程式的執行,並且這種情形會在任何一種GC演演算法中發生。當Stop-the-world發生時,除了GC所需的執行緒以外,所有執行緒都處於等待狀態直到GC任務完成。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間,從而使系統具有高吞吐 、低停頓的特點。

4.3.1. 複製演演算法(Copying)

該演演算法將記憶體平均分成兩部分,然後每次只使用其中的一部分,當這部分記憶體滿的時候,將記憶體中所有存活的物件複製到另一個記憶體中,然後將之前的記憶體清空,只使用這部分記憶體,迴圈下去。

優點:

  • 實現簡單
  • 不產生記憶體碎片

缺點:

  • 將記憶體縮小為原來的一半,浪費了一半的記憶體空間,代價太高;如果不想浪費一半的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演演算法。

  • 如果物件的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有物件都複製一遍,並將所有參照地址重置一遍。複製這一工作所花費的時間,在物件存活率達到一定程度時,將會變的不可忽視。 所以從以上描述不難看出,複製演演算法要想使用,最起碼物件的存活率要非常低才行,而且最重要的是,我們必須要克服50%記憶體的浪費。

年輕代中使用的是Minor GC,這種GC演演算法採用的是複製演演算法(Copying)。

​ HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。預設比例為8:1:1,一般情況下,新建立的物件都會被分配到Eden區。因為年輕代中的物件基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收演演算法使用的是複製演演算法。

​ 在GC開始的時候,物件只會存在於Eden區和名為「From」的Survivor區,Survivor區「To」是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到「To」,而在「From」區中,仍存活的物件會根據他們的年齡值來決定去向。物件在Survivor區中每熬過一次Minor GC,年齡就會增加1歲。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到「To」區域。經過這次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿之後,會將所有物件移動到年老代中。

因為Eden區物件一般存活率較低,一般的,使用兩塊10%的記憶體作為空閒和活動區間,而另外80%的記憶體,則是用來給新建物件分配記憶體的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden物件轉移到10%的to空閒區間,接下來,將之前90%的記憶體全部釋放,以此類推。

4.3.2. 標記清除(Mark-Sweep)

「標記-清除」(Mark Sweep)演演算法是幾種GC演演算法中最基礎的演演算法,是因為後續的收集演演算法都是基於這種思路並對其不足進行改進而得到的。正如名字一樣,演演算法分為2個階段:

  1. 標記出需要回收的物件,使用的標記演演算法均為可達性分析演演算法

  2. 回收被標記的物件。

缺點:

  • 效率問題(兩次遍歷)

  • 空間問題(標記清除後會產生大量不連續的碎片。JVM就不得不維持一個記憶體的空閒列表,這又是一種開銷。而且在分配陣列物件的時候,尋找連續的記憶體空間會不太好找。)

4.3.3. 標記壓縮(Mark-Compact)

標記-整理法是標記-清除法的一個改進版。同樣,在標記階段,該演演算法也將所有物件標記為存活和死亡兩種狀態;不同的是,在第二個階段,該演演算法並沒有直接對死亡的物件進行清理,而是通過所有存活對像都向一端移動,然後直接清除邊界以外的記憶體。

優點:

​ 標記/整理演演算法不僅可以彌補標記/清除演演算法當中,記憶體區域分散的缺點,也消除了複製演演算法當中,記憶體減半的高額代價。

缺點:

​ 如果存活的物件過多,整理階段將會執行較多複製操作,導致演演算法效率降低。

老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

4.3.4. 分代收集演演算法(Generational-Collection)

記憶體效率:複製演演算法>標記清除演演算法>標記整理演演算法(此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)。
記憶體整齊度:複製演演算法=標記整理演演算法>標記清除演演算法。
記憶體利用率:標記整理演演算法=標記清除演演算法>複製演演算法。

可以看出,效率上來說,複製演演算法是當之無愧的老大,但是卻浪費了太多記憶體,而為了儘量兼顧上面所提到的三個指標,標記/整理演演算法相對來說更平滑一些,但效率上依然不盡如人意,它比複製演演算法多了一個標記的階段,又比標記/清除多了一個整理記憶體的過程

難道就沒有一種最優演演算法嗎?

回答:無,沒有最好的演演算法,只有最合適的演演算法。==========>分代收集演演算法

分代回收演演算法實際上是把複製演演算法和標記整理法的結合,並不是真正一個新的演演算法,一般分為:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要進行回收的,新生代就是有很多的記憶體空間需要回收,所以不同代就採用不同的回收演演算法,以此來達到高效的回收演演算法。

年輕代(Young Gen)

年輕代特點是區域相對老年代較小,對像存活率低。

​ 這種情況複製演演算法的回收整理,速度是最快的。複製演演算法的效率只和當前存活對像大小有關,因而很適用於年輕代的回收。而複製演演算法記憶體利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。

老年代(Tenure Gen)

老年代的特點是區域較大,對像存活率高。

​ 這種情況,存在大量存活率高的對像,複製演演算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。

4.4. 垃圾收集器(瞭解)

如果說收集演演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實現

4.4.1. Serial/Serial Old收集器

序列收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收。新生代、老年代使用序列回收;新生代複製演演算法、老年代標記-壓縮;垃圾收集的過程中會Stop The World(服務暫停)

它還有對應老年代的版本:Serial Old

引數控制: -XX:+UseSerialGC 序列收集器

4.4.2. ParNew 收集器

ParNew收集器收集器其實就是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演演算法、Stop The world、物件分配規則、回收策略等都與Serial收集器完全一樣,實現上這兩種收集器也共用了相當多的程式碼。ParNew收集器的工作過程如下圖所示。

ParNew收集器 ParNew收集器其實就是Serial收集器的多執行緒版本。新生代並行,老年代序列;新生代複製演演算法、老年代標記-壓縮

引數控制:

-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制執行緒數量

4.4.3. Parallel / Parallel Old 收集器

Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。可以通過引數來開啟自適應調節策略,虛擬機器器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或最大的吞吐量;也可以通過引數控制GC的時間不大於多少毫秒或者比例;新生代複製演演算法、老年代標記-壓縮

引數控制: -XX:+UseParallelGC 使用Parallel收集器+ 老年代序列

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和「標記-整理」演演算法。這個收集器是在JDK 1.6中才開始提供

引數控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代並行

4.4.4. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在網際網路站或B/S系統的伺服器端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。

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

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

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

由於整個過程中耗時最長的並行標記和並行清除過程中,收集器執行緒都可以與使用者執行緒一起工作,所以總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起並行地執行。老年代收集器(新生代使用ParNew)

優點: 並行收集、低停頓
缺點: 產生大量空間碎片、並行階段會降低吞吐量

引數控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次碎片整理;整理過程是獨佔的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction 設定進行幾次Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads 設定CMS的執行緒數量(一般情況約等於可用CPU數量)

cms是一種預處理垃圾回收器,它不能等到old記憶體用盡時回收,需要在記憶體用盡前,完成回收操作,否則會導致並行回收失敗

4.4.5. G1收集器

G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中釋出的CMS收集器。與CMS收集器相比G1收集器有以下特點:

  1. 並行與並行:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過並行的方式讓java程式繼續執行。

  2. 分代收集:分代概念在G1中依然得以保留。雖然G1可以不需要其它收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。也就是說G1可以自己管理新生代和老年代了。

  3. 空間整合:由於G1使用了獨立區域(Region)概念,G1從整體來看是基於「標記-整理」演演算法實現收集,從區域性(兩個Region)上來看是基於「複製」演演算法實現的,但無論如何,這兩種演演算法都意味著G1運作期間不會產生記憶體空間碎片。

  4. 可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。

每個Region被標記了E、S、O和H,說明每個Region在執行時都充當了一種角色,其中H是以往演演算法中沒有的,它代表Humongous,這表示這些Region儲存的是巨型物件(humongous object,H-obj),當新建物件大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,並標記為H。

為了避免全堆掃描,G1使用了Remembered Set來管理相關的物件參照資訊。當進行記憶體回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏了。

如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下幾個步驟:

1、初始標記(Initial Making)

2、並行標記(Concurrent Marking)

3、最終標記(Final Marking)

4、篩選回收(Live Data Counting and Evacuation)

看上去跟CMS收集器的運作過程有幾分相似,不過確實也這樣。初始階段僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top Mark Start)的值,讓下一階段使用者程式並行執行時,能在正確可以用的Region中建立新物件,這個階段需要停頓執行緒,但耗時很短。並行標記階段是從GC Roots開始對堆中物件進行可達性分析,找出存活物件,這一階段耗時較長但能與使用者執行緒並行執行。而最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但可並行執行。最後篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這一過程同樣是需要停頓執行緒的,但Sun公司透露這個階段其實也可以做到並行,但考慮到停頓執行緒將大幅度提高收集效率,所以選擇停頓。下圖為G1收集器執行示意圖:

4.4.6. 垃圾回收器比較

如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機器器所處的區域則表示它是屬於新生代還是老年代收集器。

整堆收集器: G1

垃圾回收器選擇策略 :

使用者端程式 : Serial + Serial Old;

吞吐率優先的伺服器端程式(比如:計算密集型) : Parallel Scavenge + Parallel Old;

響應時間優先的伺服器端程式 :ParNew + CMS。

G1收集器是基於標記整理演演算法實現的,不會產生空間碎片,可以精確地控制停頓,將堆劃分為多個大小固定的獨立區域,並跟蹤這些區域的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(Garbage First)。