《面試補習》- JVM知識點大梳理

概述

一、什麼是虛擬機?

Java 虛擬機,是一個能夠執行 Java 字節碼的虛擬機進程。Java 源文件被編譯成能被 Java 虛擬機執行的字節碼文件( .class )。html

跨平臺的是 Java 程序(包括字節碼文件),,而不是 JVM。JVM 是用 C/C++ 開發的,是編譯後的機器碼,不能跨平臺,不一樣平臺下須要安裝不一樣版本的 JVM 。java

二、JVM 組成部分

  • 類加載器,在 JVM 啓動時或者類運行時將須要的 class 加載到 JVM 中。算法

  • 內存區,將內存劃分紅若干個區以模擬實際機器上的存儲、記錄和調度功能模塊,如實際機器上的各類功能的寄存器或者 PC 指針的記錄器等。數組

  • 執行引擎,執行引擎的任務是負責執行 class 文件中包含的字節碼指令,至關於實際機器上的 CPU 。緩存

  • 本地方法調用,調用 C 或 C++ 實現的本地方法的代碼返回結果。安全

一、類加載器

從類被加載到虛擬機內存中開始,到卸御出內存爲止,它的整個生命週期分爲7個階段,加載(Loading)驗證(Verification)準備(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸御(Unloading)。其中驗證、準備、解析三個部分統稱爲鏈接。 7個階段發生的順序以下:bash

加載(Loading)、驗證(Verification)、準備(Preparation)、初始化(Initialization)、卸載(Unloading) 這五個階段的過程是固定的,在類加載過程當中必須按照這種順序循序漸進地進行,而解析階段則不必定,他在某種狀況下能夠在初始化以後進行,這個是爲了支持Java語言的運行時綁定(也稱爲動態綁定或者晚期綁定)。數據結構

1.一、加載

加載階段,虛擬機須要完成3件事:多線程

  • 經過一個類的全限定名獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉換爲方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據結構的訪問入口。
加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。 
複製代碼

1.二、驗證

驗證階段主要,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,而且不會危害虛擬機自身的安全。併發

驗證階段主要完成下面4個階段的校驗動做:

文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。

元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。

字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。

符號引用驗證:確保解析動做能正確執行。
複製代碼

1.三、準備

準備階段是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區分配。

進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。

初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等)
複製代碼

1.四、解析

解析階段是將虛擬機常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行.

符號引用:簡單的理解就是字符串,好比引用一個類,java.util.ArrayList 這就是一個符號引用,字符串引用的對象不必定被加載。

直接引用:指針或者地址偏移量。引用對象必定在內存(已經加載)。
複製代碼

1.五、初始化

類初始化是類加載的最後一步,除了加載階段,用戶能夠經過自定義的類加載器參與,其餘階段都徹底由虛擬機主導和控制。到了初始化階段才真正執行Java代碼。

類的初始化的主要工做是爲靜態變量賦程序設定的初值

如static int a = 100;在準備階段,a被賦默認值0,在初始化階段就會被賦值爲100。
複製代碼

Java虛擬機規範中嚴格規定了有且只有五種狀況必須對類進行初始化:

  • 一、使用new字節碼指令建立類的實例,或者使用getstatic、putstatic讀取或設置一個靜態字段的值(放入常量池中的常量除外),或者調用一個靜態方法的時候,對應類必須進行過初始化。

  • 二、經過java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則要首先進行初始化。

  • 三、當初始化一個類的時候,若是發現其父類沒有進行過初始化,則首先觸發父類初始化。

  • 四、當虛擬機啓動時,用戶須要指定一個主類(包含main()方法的類),虛擬機會首先初始化這個類。

  • 五、使用jdk1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有進行初始化,則須要先觸發其初始化。

二、對象的建立過程

Java 中對象的建立就是在堆上分配內存空間的過程,此處說的對象建立僅限於 new 關鍵字建立的普通 Java 對象,不包括數組對象的建立。

當虛擬機遇到一條含有new的指令時,會進行一系列對象建立的操做:

2.一、檢查類是否被加載

一、檢查常量池中是否有即將要建立的這個對象所屬的類的符號引用; 若常量池中沒有這個類的符號引用,說明這個類尚未被定義!拋出ClassNotFoundException;

二、進而檢查這個符號引用所表明的類是否已經被JVM加載; 若該類尚未被加載,就找該類的class文件,並加載進方法區; 若該類已經被JVM加載,則準備爲對象分配內存;

2.二、爲對象分配內存

三、根據方法區中該類的信息肯定該類所需的內存大小; 一個對象所需的內存大小是在這個對象所屬類被定義完就能肯定的!且一個類所生產的全部對象的內存大小是同樣的!JVM在一個類被加載進方法區的時候就知道該類生產的每個對象所須要的內存大小。 四、從堆中劃分一塊對應大小的內存空間給新的對象; 分配堆中內存有兩種方式: 指針碰撞 若是JVM的垃圾收集器採用複製算法或標記-整理算法,那麼堆中空閒內存是完整的區域,而且空閒內存和已使用內存之間由一個指針標記。那麼當爲一個對象分配內存時,只需移動指針便可。所以,這種在完整空閒區域上經過移動指針來分配內存的方式就叫作「指針碰撞」。 空閒列表 若是JVM的垃圾收集器採用標記-清除算法,那麼堆中空閒區域和已使用區域交錯,所以須要用一張「空閒列表」來記錄堆中哪些區域是空閒區域,從而在建立對象的時候根據這張「空閒列表」找到空閒區域,並分配內存。 綜上所述:JVM究竟採用哪一種內存分配方法,取決於它使用了何種垃圾收集器。

多線程併發時會出現正在給對象 A 分配內存,還沒來得及修改指針,對象 B 又用這個指針分配內存,這樣就出現問題了。
解決這種問題有兩種方案:
第一種,是採用同步的辦法,使用 CAS 來保證操做的原子性。
另外一種,是每一個線程分配內存都在本身的空間內進行,便是每一個線程都在堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB),分配內存的時候再TLAB上分配,互不干擾。能夠經過 -XX:+/-UseTLAB 參數決定。
複製代碼

2.三、爲分配的內存空間初始化零值

五、爲對象中的成員變量賦上初始值(默認初始化);

對象的內存分配完成後,還須要將對象的內存空間都初始化爲零值,這樣能保證對象即便沒有賦初值,也能夠直接使用

2.四、爲對象進行其餘設置

六、設置對象頭中的信息;

所屬的類,類的元數據信息,對象的 hashcode ,GC 分代年齡等信息

2.五、執行 init 方法

七、調用對象的構造函數進行初始化

執行完上面的步驟以後,在虛擬機裏這個對象就算建立成功了,可是對於 Java 程序來講還須要執行 init 方法纔算真正的建立完成,由於這個時候對象只是被初始化零值了,尚未真正的去根據程序中的代碼分配初始值,調用了 init 方法以後,這個對象才真正能使用。

初始化順序:

在new B一個實例時首先要進行類的裝載。(類只有在使用New調用建立的時候纔會被java類裝載器裝入)

在裝載類時,先裝載父類A,再裝載子類B

裝載父類A後,完成靜態動做(包括靜態代碼和變量,它們的級別是相同的,按照代碼中出現的順序初始化)

裝載子類B後,完成靜態動做

類裝載完成,開始進行實例化

在實例化子類B時,先要實例化父類A2,實例化父類A時,先成員實例化(非靜態代碼)
父類A的構造方法
子類B的成員實例化(非靜態代碼)
子類B的構造方法

先初始化父類的靜態代碼--->初始化子類的靜態代碼-->初始化父類的非靜態代碼--->初始化父類構造函數--->初始化子類非靜態代碼--->初始化子類構造函數

複製代碼

三、對象的內存佈局

3.一、對象頭(markword)

  • 第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳、對象分代年齡,這部分信息稱爲「Mark Word」;Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據本身的狀態複用本身的存儲空間。
  • 第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例;
Klass Word  這裏實際上是虛擬機設計的一個oop-klass model模型,這裏的OOP是指Ordinary Object Pointer(普通對象指針),看起來像個指針其實是藏在指針裏的對象。而 klass 則包含 元數據和方法信息,用來描述 Java 類。它在64位虛擬機開啓壓縮指針的環境下佔用 32bits 空間。
複製代碼
  • 若是對象是一個 Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。由於虛擬機能夠經過普通 Java 對象的元數據信息肯定 Java 對象的大小,可是從數組的元數據中沒法肯定數組的大小。

在32位系統下,對象頭8字節,64位則是16個字節【未開啓壓縮指針,開啓後12字節】。

假設當前爲32bit,在對象未被鎖定狀況下。25bit爲存儲對象的哈希碼、4bit用於存儲分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0。

不一樣狀態下存放數據:

這其中鎖標識位須要特別關注下。鎖標誌位與是否爲偏向鎖對應到惟一的鎖狀態。

鎖的狀態分爲四種無鎖狀態、偏向鎖、輕量級鎖和重量級鎖

不一樣狀態時對象頭的區間含義,如圖所示。

3.二、實例數據(Instance Data)

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。

這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在 Java 源碼中定義順序的影響。

分配策略:相同寬度的字段老是放在一塊兒,好比double和long

3.三、對其填充(Padding)

對齊填充不是必然存在的,沒有特別的含義,它僅起到佔位符的做用。

因爲HotSpot規定對象的大小必須是8的整數倍,對象頭恰好是整數倍,若是實例數據不是的話,就須要佔位符對齊填充。
複製代碼

3.四、預估對象大小

32 位系統下,當使用 new Object() 時,JVM 將會分配 8(Mark Word+類型指針) 字節的空間,128 個 Object 對象將佔用 1KB 的空間。 若是是 new Integer(),那麼對象裏還有一個 int 值,其佔用 4 字節,這個對象也就是 8+4=12 字節,對齊後,該對象就是 16 字節。

以上只是一些簡單的對象,那麼對象的內部屬性是怎麼排布的?

Class A {
    int i;
    byte b;
    String str;
}
複製代碼

其中對象頭部佔用 ‘Mark Word’4 + ‘類型指針’4 = 8 字節;byte 8 位長,佔用 1 字節;int 32 位長,佔用 4 字節;String 只有引用,佔用 4 字節; 那麼對象 A 一共佔用了 8+1+4+4=17 字節,按照 8 字節對齊原則,對象大小也就是 24 字節。

這個計算看起來是沒有問題的,對象的大小也確實是 24 字節,可是對齊(padding)的位置並不對:

在 HotSpot VM 中,對象排布時,間隙是在 4 字節基礎上的(在 32 位和 64 位壓縮模式下),上述例子中,int 後面的 byte,空隙只剩下 3 字節,接下來的 String 對象引用須要 4 字節來存放,所以 byte 和對象引用之間就會有 3 字節對齊,對象引用排布後,最後會有 4 字節對齊,所以結果上依然是 7 字節對齊。此時對象的結構示意圖,以下圖所示:

四、對象訪問

對象的訪問方式由虛擬機決定,java虛擬機提供兩種主流的方式
1.句柄訪問對象
2.直接指針訪問對象。(Sun HotSpot使用這種方式)
複製代碼

4.一、句柄訪問

簡單來講就是java堆劃出一塊內存做爲句柄池,引用中存儲對象的句柄地址,句柄中包含對象實例數據、類型數據的地址信息。

優勢:引用中存儲的是穩定的句柄地址,在對象被移動【垃圾收集時移動對象是常態】只需改變句柄中實例數據的指針,不須要改動引用【ref】自己。

4.二、直接指針

與句柄訪問不一樣的是,ref中直接存儲的就是對象的實例數據,可是類型數據跟句柄訪問方式同樣。

優勢:優點很明顯,就是速度快,相比於句柄訪問少了一次指針定位的開銷時間。【多是出於Java中對象的訪問時十分頻繁的,平時咱們經常使用的JVM HotSpot採用此種方式】

五、JVM 內存區域

5.一、虛擬機棧

描述的是方法執行時的內存模型,是線程私有的,生命週期與線程相同,每一個方法被執行的同時會建立棧楨,主要保存執行方法時的局部變量表、操做數棧、動態鏈接和方法返回地址等信息,方法執行時入棧,方法執行完出棧,出棧就至關於清空了數據,入棧出棧的時機很明確,因此這塊區域不須要進行 GC。

Java虛擬機棧可能出現兩種類型的異常:

  • 線程請求的棧深度大於虛擬機容許的棧深度,將拋出StackOverflowError

  • 虛擬機棧空間能夠動態擴展,當動態擴展是沒法申請到足夠的空間時,拋出OutOfMemory異常

  • 拓展link: 棧幀

5.二、本地方法棧

與虛擬機棧功能很是相似,主要區別在於虛擬機棧爲虛擬機執行 Java 方法時服務,而本地方法棧爲虛擬機執行本地方法時服務的。這塊區域也不須要進行 GC。

5.三、程序計數器

  • 程序計數器是一塊很小的內存空間,它是線程私有的,能夠認做爲當前線程的行號指示器。
  • 程序計數器的主要做用是記錄線程運行時的狀態,方便線程被喚醒時能從上一次被掛起時的狀態繼續執行
  • 程序計數器是惟一一個在 Java 虛擬機規範中沒有規定任何 OOM 狀況的區域,因此這塊區域也不須要進行 GC

5.四、本地內存

  • 線程共享區域,Java 8 中,本地內存,也是咱們一般說的堆外內存,包括元空間和方法區
  • 主要存儲類的信息,常量,靜態變量,即時編譯器編譯後代碼等,這部分因爲是在堆中實現的,受 GC 的管理,不過因爲永久代有 -XX:MaxPermSize 的上限
  • 因此若是動態生成類(將類信息放入永久代)或大量地執行 String.intern (將字段串放入永久代中的常量區),很容易形成 OOM,有人說能夠把永久代設置得足夠大,但很難肯定一個合適的大小,受類數量,常量數量的多少影響很大。
  • 因此在 Java 8 中就把方法區的實現移到了本地內存中的元空間中,這樣方法區就不受 JVM 的控制了,也就不會進行 GC,也所以提高了性能(發生 GC 會發生 Stop The Word,形成性能受到必定影響,後文會提到),也就不存在因爲永久代限制大小而致使的 OOM 異常了(假設總內存2G,JVM 被分配內存 100M, 理論上元空間能夠分配 2G-100M = 1.9G,空間大小足夠),也方便在元空間中統一管理。
  • 綜上所述,在 Java 8 之後這一區域也不須要進行 GC
  • 拓展link: 堆外內存回收

5.五、堆

  • 對象實例和數組都是在堆上分配的,GC 也主要對這兩類數據進行回收。
  • java虛擬機規範對這塊的描述是:全部對象實例及數組都要在堆上分配內存,但隨着JIT編譯器的發展和逃逸分析技術的成熟,這個說法也不是那麼絕對,可是大多數狀況都是這樣的。
  • 堆細分: 新生代(Eden,survior)和老年代

六、對象存活判斷

  • 引用計數
  • 可達性分析

6.一、引用計數

每一個對象有一個引用計數屬性,新增一個引用時計數加 1 ,引用釋放時計數減 1 ,計數爲 0 時能夠回收。此方法簡單,沒法解決對象相互循環引用的問題。目前在用的有 Python、ActionScript3 等語言。

6.二、可達性分析

從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。不可達對象。目前在用的有 Java、C# 等語言。

GC Roots 對象:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中的類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中 JNI(即通常說的 Native 方法)中引用的對象。
複製代碼

如何判斷無用的類:

該類全部實例都被回收(Java 堆中沒有該類的對象)。
加載該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方利用反射訪問該類。
複製代碼

6.三、finalize

finallize()方法,是在釋放該對象內存前由 GC (垃圾回收器)調用。

一般建議在這個方法中釋放該對象持有的資源,例如持有的堆外內存、和遠程服務的長鏈接。 通常狀況下,不建議重寫該方法。 對於一個對象,該方法有且僅會被調用一次。

6.四、對象引用類型

  • 強引用
  • 軟引用(SoftReference)
  • 弱引用(WeakReference)
  • 虛引用(PhantomReference)

6.4.一、強引用

若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題

6.4.二、軟引用

若是一個對象只具備軟引用,那就相似於無關緊要的生活用品。若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。

6.4.三、弱引用

弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。

6.4.四、虛引用

「虛引用」顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。。當垃 圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序若是發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。

拓展

利用軟引用和弱引用解決 OOM 問題。用一個 HashMap 來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關係,在內存不足時,JVM 會自動回收這些緩存圖片對象所佔用的空間,從而有效地避免了 OOM 的問題. 經過軟引用實現 Java 對象的高速緩存。好比咱們建立了一 Person 的類,若是每次須要查詢一我的的信息,哪怕是幾秒中以前剛剛查詢過的,都要從新構建一個實例,這將引發大量 Person 對象的消耗,而且因爲這些對象的生命週期相對較短,會引發屢次 GC 影響性能。此時,經過軟引用和 HashMap 的結合能夠構建高速緩存,提供性能。

七、垃圾回收算法

  • 標記-清除算法
  • 標記-整理算法
  • 複製算法
  • 分代收集算法

7.一、標記-清除

在標記階段,首先經過根節點,標記全部從根節點開始的可達對象。所以,未被標記的對象就是未被引用的垃圾對象(好多資料說標記出要回收的對象,其實明白大概意思就能夠了)。而後,在清除階段,清除全部未被標記的對象。

缺點:

  • 一、效率問題,標記和清除兩個過程的效率都不高。
  • 二、空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大的對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

7.二、標記-整理

標記整理算法,相似與標記清除算法,不過它標記完對象後,不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。

優勢:

  • 一、相對標記清除算法,解決了內存碎片問題。
  • 二、沒有內存碎片後,對象建立內存分配也更快速了(可使用TLAB進行分配)。

缺點:

  • 一、效率問題,(同標記清除算法)標記和整理兩個過程的效率都不高。

7.三、複製算法

複製算法,能夠解決效率問題,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已經使用過的內存空間一次清理掉,這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可(還可以使用TLAB進行高效分配內存)

優勢:

  • 一、效率高,沒有內存碎片。

缺點:

  • 一、浪費一半的內存空間。
  • 二、複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。

7.四、分代算法

當前商業虛擬機都是採用分代收集算法,它根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把 Java 堆分爲新生代和老年代,而後根據各個年代的特色採用最適當的收集算法。

在新生代中,每次垃圾收集都發現有大批對象死去,只有少許存活,就選用複製算法。 而老年代中,由於對象存活率高,沒有額外空間對它進行分配擔保,就必須使用「標記清理」或者「標記整理」算法來進行回收。

圖的左半部分是未回收前的內存區域,右半部分是回收後的內存區域。

對象分配策略: 對象優先在 Eden 區域分配,若是對象過大直接分配到 Old 區域。 長時間存活的對象進入到 Old 區域。

改進自複製算法

如今的商業虛擬機都採用這種收集算法來回收新生代,IBM 公司的專門研究代表,新生代中的對象 98% 是「朝生夕死」的,因此並不須要按照 1:1 的比例來劃份內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。 HotSpot 虛擬機默認 Eden 和 2 塊 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用內存空間爲整個新生代容量的 90%(80%+10%),只有 10% 的內存會被「浪費」。固然,98% 的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於 10% 的對象存活,當 Survivor 空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保(Handle Promotion)。

八、安全點

8.一、安全點

SafePoint 安全點,顧名思義是指一些特定的位置,當線程運行到這些位置時,線程的一些狀態能夠被肯定(the thread’s representation of it’s Java machine state is well described),好比記錄OopMap 的狀態,從而肯定 GC Root 的信息,使 JVM 能夠安全的進行一些操做,好比開始 GC 。

SafePoint 指的特定位置主要有:

  • 循環的末尾 (防止大循環的時候一直不進入 Safepoint ,而其餘線程在等待它進入 Safepoint )。
  • 方法返回前。
  • 調用方法的 Call 以後。
  • 拋出異常的位置。

8.二、安全區域

安全點完美的解決了如何進入GC問題,實際狀況可能比這個更復雜,可是若是程序長時間不執行,好比線程調用的sleep方法,這時候程序沒法響應JVM中斷請求這時候線程沒法到達安全點,顯然JVM也不可能等待程序喚醒,這時候就須要安全區域了。

安全區域是指一段代碼片中,引用關係不會發生變化,在這個區域任何地方GC都是安全的,安全區域能夠看作是安全點的一個擴展。線程執行到安全區域的代碼時,首先標識本身進入了安全區域,這樣GC時就不用管進入安全區域的線層了,線層要離開安全區域時就檢查JVM是否完成了GC Roots枚舉,若是完成就繼續執行,若是沒有完成就等待直到收到能夠安全離開的信號。

九、JVM 垃圾回收器

收集器 串行、並行or併發 新生代/老年代 算法 目標 適用場景
Serial 串行 新生代 複製算法 響應速度優先 單CPU環境下的Client模式
Serial Old 串行 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後備預案
ParNew 並行 新生代 複製算法 響應速度優先 多CPU環境時在Server模式下與CMS配合
Parallel Scavenge 並行 新生代 複製算法 吞吐量優先 在後臺運算而不須要太多交互的任務
Parallel Old 並行 老年代 標記-整理 吞吐量優先 在後臺運算而不須要太多交互的任務
CMS 併發 老年代 標記-清除 響應速度優先 集中在互聯網站或B/S系統 服務端上的Java應用
G1 併發 both 標記-整理+複製算法 響應速度優先 面向服務端應用,未來替換CMS
ZGC 併發 both 標記-整理+複製算法 響應速度優先 面向服務端應用,未來替換CMS

9.一、Serial (新生代)

  • 最基本的單線程垃圾收集器。使用一個CPU或一條收集線程去執行垃圾收集工做。
  • 工做時會Stop The World,暫停全部用戶線程,形成卡頓。適合運行在Client模式下的虛擬機。
  • 用做新生代收集器,複製算法。

9.二、ParNew(新生代)

  • Serial收集器的多線程版本,和Serial的惟一區別就是使用了多條線程去垃圾收集。
  • 除了Serial,只有它能夠和CMS搭配使用的收集器。
  • 用做新生代收集器,複製算法。

9.三、Parallel Scavenge(新生代)

用做新生代收集器,複製算法。 關注高吞吐量,能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。 Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

9.四、Serial Old(老年代)

  • Serial收集器的老年代版本,單線程,標記-整理 算法。
  • 通常用於Client模式的虛擬機。
  • 當虛擬機是Server模式時,有2個用途:一種用途是在JDK 1.5以及以前的版本中與Parallel Scavenge收集器搭配使用 ,另外一種用途就是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

9.五、Parallel Old(老年代)

  • Parallel Scavenge收集器的老年代版本,使用多線程和 標記-整理 算法。在JDK 1.6中開始提供。 在注重吞吐量的場合,配合Parallel Scavenge收集器使用。

9.六、CMS(Concurrent Mark Sweep)(老年代)

  • 一種以獲取最短回收停頓時間爲目標的收集器。適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。
  • 基於 標記—清除 算法。適合做爲老年代收集器。
  • 收集過程分4步:
一、 初始標記(CMS initial mark):只是標記一下GC Roots能直接關聯到的對象,速度很快,會Stop The World。
二、 併發標記(CMS concurrent mark):進行GC Roots Tracing(可達性分析)的過程。
三、 從新標記(CMS remark):會Stop The -World。爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常比初始標記階段稍長些,但遠比並發標記的時間短。
四、 併發清除(CMS concurrent sweep):回收內存。
複製代碼

耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此時併發執行的。

缺點:

  • 併發階段,雖然不會致使用戶線程暫停,但會佔用一部分線程(CPU資源),致使應用變慢,吞吐量下降。默認啓動收集線程數是(CPU數量+3)/4。即當CPU在4個以上時,併發回收時垃圾收集線程很多於25%的CPU資源,而且隨着CPU數量的增長而降低。可是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大。
  • 沒法清除浮動垃圾。併發清除階段,用戶線程還在運行,還會產生新垃圾。這些垃圾不會在這次GC中被標記,只能等到下次GC被回收。
  • 標記-清除 算法會產生大量不連續內存,致使分配大對象時內存不夠,提早觸發Full GC。

9.七、G1

  • 在JDK1.7提供的先進垃圾收集器。
  • 既適合新生代,也適合老年代。
  • 空間整合:使用 標記-整理 算法,不產生碎片空間。
  • 整個Java堆被分爲多個大小相同的的塊(region)。新生代和老年代再也不是物理隔離的,而是一部分region塊組成的集合。
  • 默認把堆平均分紅2048個region,最小1M,最大32M,必須是2的冪次方,能夠經過-XX:G1HeapRegionSize參數指定。region分爲4種:
E:eden區,新生代
S:survivor區,新生代
O:old區,老年代
H:humongous區,用來放大對象。當新建對象大小超過region大小一半時,直接在新的一個或多個連續region中分配,並標記爲H
複製代碼
  • 可預測的停頓時間:估算每一個region內的垃圾可回收的空間以及回收須要的時間(經驗值),記錄在一個優先列表中。收集時,優先回收價值最大的region,而不是在整個堆進行全區域回收。這樣提升了回收效率,得名:Garbage-First。 G1中有2種GC:

  • young GC:新生代eden區沒有足夠可用空間時觸發。存活的對象移到survivor區或晉升old區。 mixed GC:當old區對象不少時,老年代對象空間佔堆總空間的比值達到閾值(-XX:InitiatingHeapOccupancyPercent默認45%)會觸發,它除了回收年輕代,也回收 部分 老年代(回收價值高的部分region)。

mixed GC回收步驟:

一、初始標記(Initial Marking):只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark 二、Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象。這階段須要停頓線程(STW),但耗時很短,共用YGC的停頓,因此通常伴隨着YGC發生。
三、併發標記(Concurrent Marking):進行可達性分析,找出存活對象,耗時長,但可與用戶線程併發執行。
四、最終標記(Final Marking):修正併發標記階段用戶線程運行致使的變更記錄。會STW,但能夠並行執行,時間不會很長。
五、篩選回收(Live Data Counting and Evacuation):根據每一個region的回收價值和回收成本排序,根據用戶配置的GC停頓時間開始回收。
複製代碼
  • 當對象分配過快,mixed GC來不及回收,G1會退化,觸發Full GC,它使用單線程的Serial收集器來回收,整個過程STW,要儘可能避免這種狀況。
  • 當內存不多的時候(存活對象佔用大量空間),沒有足夠空間來複制對象,會致使回收失敗。這時會保留被移動過的對象和沒移動的對象,只調整引用。失敗發生後,收集器認爲存活對象被移動了,有足夠空間讓應用程序使用,因而用戶線程繼續工做,等待下一次觸發GC。若是內存不夠,就會觸發Full GC。

9.八、ZGC

在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。

ZGC幾乎在全部地方併發執行的,除了初始標記的是STW的。因此停頓時間幾乎就耗費在初始標記上,這部分的實際是很是少的。那麼其餘階段是怎麼作到能夠併發執行的呢?

ZGC主要新增了兩項技術,

  • 着色指針Colored Pointer,
  • 讀屏障Load Barrier。

ZGC 是一個併發、基於區域(region)、增量式壓縮的收集器。Stop-The-World 階段只會在根對象掃描(root scanning)階段發生,這樣的話 GC 暫停時間並不會隨着堆和存活對象的數量而增長。

處理階段:

  • 標記(Marking);
  • 重定位(Relocation)/壓縮(Compaction);
  • 從新分配集的選擇(Relocation set selection);
  • 引用處理(Reference processing);
  • 弱引用的清理(WeakRefs Cleaning);
  • 字符串常量池(String Table)和符號表(Symbol Table)的清理;
  • 類卸載(Class unloading)

着色指針Colored Pointer

ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked一、Marked0(ZGC僅支持64位平臺),以標記該指向內存的存儲狀態。
至關於在對象的指針上標註了對象的信息。注意,這裏的指針至關於Java術語當中的引用。

在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
複製代碼

讀屏障Load Barrier

因爲着色指針的存在,在程序運行時訪問對象的時候,能夠輕易知道對象在內存的存儲狀態(經過指針訪問對象),
若請求讀的內存在被着色了,那麼則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有必定的耗費,從而達到與用戶線程併發的效果。
複製代碼

與標記對象的傳統算法相比,ZGC在指針上作標記,在訪問指針時加入Load Barrier(讀屏障),好比當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,也就是,永遠只有單個對象讀取時有機率被減速,而不存在爲了保持應用與GC一致而粗暴總體的Stop The World。

拓展

Java——七種垃圾收集器+JDK11最新ZGC

相關文章
相關標籤/搜索