JVM 原理與深度調優

什麼是 jvm

jvm 是 java 虛擬機 運行在用戶態、通過應用程序實現 java 代碼跨平臺、與平臺無關、實際上是 "一次編譯,到處執行"

  1. 從微觀來說編譯出來的是字節碼!去到哪個平臺都能用,只要有那個平臺的 JDK 就可以運行!字碼好比是一個人,平臺好比爲國家,JDK 好比這個國家的語言!只要這個人(字節碼)有了這個國家的語言(JDK)就可以在這個國家(平臺)生活下去。
    2.JDK 是整個 Java 的核心,包括了 Java 運行環境(Java Runtime Envirnment),一堆 Java 工具和 Java 基礎的類庫 (rt.jar)。
    3.Java 虛擬機(JVM)一種用於計算機設備的規範,可用不同的方式(軟件或硬件)加以實現。編譯虛擬機的指令集與編譯微處理器的指令集非常類似。Java 虛擬機包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。
    4.java 編譯出來的是一種 “java 字節碼”,由虛擬機去解釋執行。而 c 和 c++ 則編譯成了二進制,直接交由操作系統執行。
  2. 所謂的一次編譯、到處執行,即只需在一個地方編譯,在其他各個平臺下都可以執行。
  3. 與平臺無關指的是 JAVA 只運行在自己的 JVM 上,不需要依賴任何其他的底層類,所以和操作系統沒有任何聯繫,平臺是說運行的系統

內存結構圖

class 文件

class 文件徑打破了 C 或者 C++ 等語言所遵循的傳統,使用這些傳統語言寫的程序通常首先被編譯,然後被連接成單獨的、專門支持特定硬件平臺和操作系統的二進制文件。通常情況下,一個平臺上的二進制可執行文件不能在其他平臺上工作。而 Java class 文件是可以運行在任何支持 Java 虛擬機的硬件平臺和操作系統上的二進制文件。

執行過程

執行過程簡介

當編譯和連接一個 C++ 程序時,所獲得的可執行二進制文件只能在指定的硬件平臺和操作系統上運行,因爲這個二進制文件包含了對目標處理器的機器語言。而 Java 編譯器把 Java 源文件的指令翻譯成字節碼,這種字節碼就是 Java 虛擬機的 “機器語言”。

與普通程序不同的是,Java 程序(class 文件)並不是本地的可執行程序。當運行 Java 程序時,首先運行 JVM(Java 虛擬機),然後再把 Java class 加載到 JVM 裏頭運行,負責加載 Java class 的這部分就叫做 Class Loader。

JVM 中的 ClassLoader

JVM 本身包含了一個 ClassLoader 稱爲 Bootstrap ClassLoader,和 JVM 一樣,BootstrapClassLoader 是用本地代碼實現的,它負責加載核心 JavaClass(即所有 java.* 開頭的類)。

另外 JVM 還會提供兩個 ClassLoader,它們都是用 Java 語言編寫的,由 BootstrapClassLoader 加載;其中 Extension ClassLoader 負責加載擴展的 Javaclass(例如所有 javax.* 開頭的類和存放在 JRE 的 ext 目錄下的類)ApplicationClassLoader 負責加載應用程序自身的類。

當運行一個程序的時候,JVM 啓動,運行 bootstrapclassloader,該 ClassLoader 加載 java 核心 API(ExtClassLoader 和 AppClassLoader 也在此時被加載),然後調用 ExtClassLoader 加載擴展 API,最後 AppClassLoader 加載 CLASSPATH 目錄下定義的 Class,這就是一個程序最基本的加載流程。

第一個 Class 文件、通過 javac 編譯成字節碼、字節碼之後有個 ClassLoader 叫類加載器,因爲 java.class 文件到 JVM 內部運行起來需要有個裝載過程、從物理的文件到內存的結構、比如加載、連接、初始化。

linux 應用程序有個進程地址空間,對進程地址空間的解釋:

linux 採用虛擬內存管理技術,每一個進程都有一個 3G 大小的獨立的進程地址空間,這個地址空間就是用戶空間。每個進程的用戶空間都是完全獨立、互不相干的。進程訪問內核空間的方式:系統調用和中斷。
    創建進程等進程相關操作都需要分配內存給進程。這時進程申請和獲得的不是物理地址,僅僅是虛擬地址。 
實際的物理內存只有當進程真的去訪問新獲取的虛擬地址時,纔會由 “請頁機制” 產生 “缺頁” 異常,從而進入分配實際頁框的程序。該異常是虛擬內存機制賴以存在的基本保證,它會告訴內核去爲進程分配物理頁,並建立對應的頁表,這之後虛擬地址才實實在在的映射到了物理地址上。

Linux 操作系統採用虛擬內存技術,所有進程之間以虛擬方式共享內存。進程地址空間由每個進程中的線性地址區組成,而且更爲重要的特點是內核允許進程使用該空間中的地址。通常情況況下,每個進程都有唯一的地址空間,而且進程地址空間之間彼此互不相干。但是進程之間也可以選擇共享地址空間,這樣的進程就叫做線程。

基本上所有 linux 應用程序都會遵循這個規泛、有棧、有堆、對於 JVM 來說、也是遵循這個規則、只不過在這個規則上做了一些改進

通過類加載器把 Class 文件裝載進內存空間、裝進來以後只是你的字節碼,然後你需要去運行、怎麼去運行呢 ?圖中類加載器子系統下面都是運行區
內存空間裏有:

  1. 方法區:被裝載的 class 的信息存儲在 Methodarea 的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的 class 文件,然後讀入這個 class 文件內容並把它傳輸到虛擬機中。
    2.Heap(堆):一個 Java 虛擬實例中只存在一個堆空間。
    3.JavaStack(java 的棧):虛擬機只會直接對棧執行兩種操作:以幀爲單位的壓棧或出棧,java 棧有個核心的數據、先進後出
    4.Nativemethodstack(本地方法棧):通過字面意思、基本是調用系統本地的一些方法、一般在底層封裝好了、直接調用
  2. 地址、在這裏邊是一個指針的概念、比如從變量到對象怎麼做引用、就是地址
  3. 計數器:主要做字節碼解析的時候要記住它的位置、可以理解爲一個標記
  4. 執行引擎:數據、字節碼做一些業務處理、最終達到想要的結果
  5. 本地方法接口:基本是底層系統、比如 IO 網絡、調用操作系統本身
  6. 本地方法庫:爲了兼容、實現跨平臺有不同的庫 、兼容平臺性
    額外數據信息指的是本地方法接口和本地方法庫

JMM

java 的內存模型

大家可能聽過一個詞、叫線程安全、在寫高併發的時候就會有線程安全問題、java 裏邊爲什麼會出現線程安全問題呢、因爲有 JMM 的存在、它會把內存分爲兩個區域(一個主內存、一個是工作內存)工作內存是每個 java 棧所私有的
因爲要運行速度快、需要把主內存的數據放到本地內存中、然後進行計算、計算完以後再把數據回顯回去

JMM 有兩個區域、主內存和棧內存、
java 線程可能不止一個、可能有多個棧、現在需要三個線程同時做個運算、主內存初始值 x=0 需要把 x=0 都要裝載在自己的內存裏邊去、相當於有一個
副本、現在初始值和三個棧都是 x=0
現在需要做運算 
x=x+1
x=x-1
x=0
我們的期望值是 x=0,如果是單個線程跑沒問題 、取回 x=0、運算 x=+1、回顯進來主內存就是 1 、棧 1 是 1,運算 x=-1、回顯進來主內存就是 0、棧 1 是 0

如果多個線程同時執行、結果是不可預期的、正因爲有這種結構的存在、當執行 x=+1、棧 1 是 x=1  、棧 2 來不及執行、棧 1 就已經把 x=1 寫到主內存了 、棧 2 跟棧 3 拿過去之後初始值就不是 0、可能就是 1 了 、這樣程序就寫亂了 

所以在 java 中就出現了很多鎖、來確保線程安全 

運行時數據區

PC 寄存器 ---- 線程私有

PC 寄存器也叫程序計數器(Program Counter Register)是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的信號指示器。
每一條 JVM 線程都有自己的 PC 寄存器
在任意時刻,一條 JVM 線程只會執行一個方法的代碼。該方法稱爲該線程的當前方法(Current Method)
如果該方法是 java 方法,那 PC 寄存器保存 JVM 正在執行的字節碼指令的地址
如果該方法是 native,那 PC 寄存器的值是 undefined。
此內存區域是唯一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域。

Java 虛擬機棧 ---- 線程私有

與 PC 寄存器一樣,java 虛擬機棧(Java Virtual Machine Stack)也是線程私有的。每一個 JVM 線程都有自己的 java 虛擬機棧,這個棧與線程同時創建,它的生命週期與線程相同。
虛擬機棧描述的是 Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
JVM stack 可以被實現成固定大小,也可以根據計算動態擴展。
如果採用固定大小的 JVM stack 設計,那麼每一條線程的 JVM Stack 容量應該在線程創建時獨立地選定。JVM 實現應該提供調節 JVM Stack 初始容量的手段。
如果採用動態擴展和收縮的 JVM Stack 方式,應該提供調節最大、最小容量的手段。
JVM Stack 異常情況:
StackOverflowError:當線程請求分配的棧容量超過 JVM 允許的最大容量時拋出
OutOfMemoryError:如果 JVM Stack 可以動態擴展,但是在嘗試擴展時無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的虛擬機棧時拋出。

本地方法棧 ---- 線程私有

Java 虛擬機可能會使用到傳統的棧來支持 native 方法(使用 Java 語言以外的其它語言編寫的方法)的執行,這個棧就是本地方法棧(Native Method Stack)

如果 JVM 不支持 native 方法,也不依賴與傳統方法棧的話,可以無需支持本地方法棧。

如果支持本地方法棧,則這個棧一般會在線程創建的時候按線程分配。

異常情況:

StackOverflowError:如果線程請求分配的棧容量超過本地方法棧允許的最大容量時拋出

OutOfMemoryError:如果本地方法棧可以動態擴展,並且擴展的動作已經嘗試過,但是目前無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的本地方法棧,那 Java 虛擬機將會拋出一個 OutOfMemoryError 異常。

Jave 堆 ---- 線程公用

平時所說的 java 調優就是它
在 JVM 中,堆(heap)是可供各條線程共享的運行時內存區域,也是供所有類實例和數據對象分配內存的區域。
Java 堆載虛擬機啓動的時候就被創建,堆中儲存了各種對象,這些對象被自動管理內存系統(Automatic Storage Management System,也即是常說的 “Garbage Collector(垃圾回收器)”)所管理。這些對象無需、也無法顯示地被銷燬。
Java 堆的容量可以是固定大小,也可以隨着需求動態擴展,並在不需要過多空間時自動收縮。
Java 堆所使用的內存不需要保證是物理連續的,只要邏輯上是連續的即可。
JVM 實現應當提供給程序員調節 Java 堆初始容量的手段,對於可動態擴展和收縮的堆來說,則應當提供調節其最大和最小容量的手段。
Java 堆異常:
OutOfMemoryError:如果實際所需的堆超過了自動內存管理系統能提供的最大容量時拋出。

方法區 ---- 線程公用

方法區是可供各條線程共享的運行時內存區域。存儲了每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法

方法區在虛擬機啓動的時候創建。

方法區的容量可以是固定大小的,也可以隨着程序執行的需求動態擴展,並在不需要過多空間時自動收縮。

方法區在實際內存空間中可以是不連續的。

Java 虛擬機實現應當提供給程序員或者最終用戶調節方法區初始容量的手段,對於可以動態擴展和收縮方法區來說,則應當提供調節其最大、最小容量的手段。

Java 方法區異常:

OutOfMemoryError: 如果方法區的內存空間不能滿足內存分配請求,那 Java 虛擬機將拋出一個 OutOfMemoryError 異常。

JVM 內存分配

內存分配其實真正來講是有三種的、但對於 JVM 來說只有兩種

大家在調優的過程中會發現有個參數是 - Xss 默認是 1m,這個內存是棧內存分配, 在工作中會發現棧 OutOfMemory Error 內存溢出、就是因爲它的內存空間不夠了 一般情況下沒有那麼大的棧、除非你的一個方法裏邊有幾十萬行代碼、一直往那壓、不出,所以導致棧的溢出、棧的內存分配直接決定了你的線程數 、比如說你默認情況下是 1m 、系統一共給你 512m、那最高可以分配 512 個線程,再多系統分配不了啦、因爲沒有那麼多的內存 、像 tomcat、resin、jboss 等、有個最大線程數、要根據這個來調、調個 100 萬沒有意義、分配不了那麼大、調太少整個性能發揮不出來 ,調這個 、跟你的 cpu 有關係、需要找一個折中位置 、根據應用 、是 IO 密集型的還是 CPU 密集型的來調 - Xss 的值、它這裏邊主要保存了一些參數 、還有局部變量 、就比如說寫代碼、有開始有結束、這裏邊肯定定義了很多變量、比如:int x=1 y=0 只要在這方法內的都屬於局部變量 、因爲你要做運算、要把這東西存住、只有等程序結束的時候才能銷燬,對於這種參數是不會產生線程安全問題、因爲線程是私有的

Java 的堆是一個運行時數據區, 類的 (對象從中分配空間。這些對象通過 new、newarray、anewarray 和 multianewarray 等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因爲它是在運行時動態分配內存的,Java 的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢

jvm 堆結構

                     (圖一)

1.Young(年輕代)

年輕代分三個區。一個 Eden 區,兩個 Survivor 區。大部分對象在 Eden 區中生成。當 Eden 區滿時,還存活的對象將被複制到 Survivor 區(兩個中的一個),當這個 Survivor 區滿時,此區的存活對象將被複制到另外一個 Survivor 區,當這個 Survivor 區也滿了的時候,從第一個 Survivor 區複製過來的並且此時還存活的對象,將被複制年老區 (Old。需要注意,Survivor 的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從 Eden 複製過來對象,和從前一個 Survivor 複製過來的對象,而複製到年老區的只有從第一個 Survivor 區過來的對象。而且,Survivor 區總有一個是空的。

2.Old(年老代)

年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。

3.Permanent:(持久代)

 也叫方法區、用於存放靜態文件,如 Java 類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些 class,例如 hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過 - XX:MaxPermSize = 進行設置。

舉個例子:當在程序中生成對象時,正常對象會在年輕代中分配空間,如果是過大的對象也可能會直接在年老代生成(據觀測在運行某程序時候每次會生成一個十兆的空間用收發消息,這部分內存就會直接在年老代分配)。年輕代在空間被分配完的時候就會發起內存回收,大部分內存會被回收,一部分倖存的內存會被拷貝至 Survivor 的 from 區,經過多次回收以後如果 from 區內存也分配完畢,就會也發生內存回收然後將剩餘的對象拷貝至 to 區。等到 to 區也滿的時候,就會再次發生內存回收然後把倖存的對象拷貝至年老區。

通常我們說的 JVM 內存回收總是在指堆內存回收,確實只有堆中的內容是動態申請分配的,所以以上對象的年輕代和年老代都是指的 JVM 的 Heap 空間,而持久代則是值指 MethodArea,不屬於 Heap。

java 堆結構和垃圾回收

                    圖(二)

Direct Momery 嚴格意義來說也算堆,它是一塊物理內存、可以分爲操作系統內存、是比較快的、不會走 JVM 在 java 裏邊實現了內存映射、這樣速度更快
CodeCache 放一些字節碼、類的信息會放在裏邊
Permanent Generation space 方法區、嚴格意義來說也屬於堆
Eden Space 區
Survivor Space 區
Tenured Generation Old 區(年老代)

JVM GC 管理
調優大部分調優的是怎麼回收,Minor GC 回收 Eden Space 和 Survivor Space , Full GC 回收所有區域 
不管什麼 GC,回收過程中會出現暫停、回收過程中用戶線程是不會工作的、這樣就造成程序卡了 這是無法改變不了的事實、避免不了、不過可以優化暫停時間的長短
原則上不能出現 Full GC 、所有區域都要跑一遍 、出現 Full GC 應用就不可用

Jvm 堆配置參數

1、-Xms 初始堆大小
默認物理內存的 64/1(<1GB),建議小於 1G、可根據應用業務調節

2、-Xmx 最大堆大小
默認物理內存的 4/1(<1GB)、建議小於 1G、實際中建議不大於 4GB(否則會出現很多問題)

3、一般建議設置 -Xms= -Xmx
好處是避免每次在 gc 後、調整堆的大小、減少系統內存分配開銷

4、整個堆大小 = 年輕代大小 + 年老代大小 + 持久代大小(Permanent Generation space 區、也會被 Full GC 回收)

jvm 新生代(young generation

                                              圖(三)

1、新生代 = 1 個 eden 區和 2 個 Survivor 區
2、-Xmn 年輕代大小
設置年輕代大小、比如 - Xmn=100m 那麼新生代就是 100m,然後共享
3、-XX:NewRatio
年輕代(包括 Eden 和兩個 Survivor 區)與年老代的比值(除去持久代)Xms=Xmx 並且設置了 Xmn 的情況下,該參數不需要進行設置。
4、-XX:SurvivorRatio
Eden 區與 Survivor 區的大小比值,設置爲 8(默認是 8) ,則兩個 Survivor 區與一個 Eden 區的比值爲 2:8,一個 Survivor 區佔整個年輕代的 1/10
比如新生代 = 100m,設置 - XX:SurvivorRatio 爲 8,那 E =80m S0 =10m S1=10m(1/10)
5、用來存放 JVM 剛分配的 Java 對象

java 老年代(tenured generation)

           圖(四)

1、老年代 = 整個堆 - 年輕代大小 - 持久代大小
年輕代就是上面講的 - xmn 配置的參數、持久代參數默認是 0
2、年輕代中經過垃圾回收沒有回收掉的對象被複制到年老代。
就是這個對象收集完一次、發現被引用了、某個地方使用了、回收不掉才放進去,一般是多次回收、從 E 區回收過程中、先進 S0 或者 S1、S0 或者 S1 再回收一次、回收不掉再放到年老區
3、老年代存儲對象比年輕代年齡大的多,而且不乏大對象。
對互聯網企業來說、最常用的是 "緩存" 的對象比較多、緩存一般會用弱引用、但弱引用也不會輕易被回收的、除非是在整個堆的內存不夠的情況下、防止你的內存宕機、強引用是和垃圾回收機制相關的。一般的,如果一個對象可以通過一系列的強引用引用到,那麼就 說明它是不會被垃圾回收機制(Garbage Collection)回收的,
剛纔說了緩存對象一般是弱引用、有些數據丟了是沒關係的、只是提高你的系統性能才放到緩存裏邊去、但是如果有一天內存不夠了 、緩存佔了很大一部分對象、你不回收的話、你整個系統都不可用了、整個服務都不能用了、如果回收掉、我可以從數據庫去取、可 能速 度慢點、但是我的服務可用性不會降低
比如說剛開始分配的對象 、這個對象暫定是 OLD 區、剛開始一部分內存區域被緩存佔據了、一般情況下對於一個緩存的設計都有初始值、對於 java 來說、比較通用的緩存是可以自動伸縮的、
如圖(四)整個 OLD 區 50M 有 45M 是被緩存佔據了、不會被回收掉、那整個 OLD 區只有 5M 可以用了 、假如 E 區有 40M 、S0 分配 10M 、S1 分配也是 10M 、理想情況下、經過 E 區到 S0、S1 到老年代的大小不到 1M、 那 5M 就夠了、不會出現 FULL GC 、也不會出現       內存溢出、一旦你的對象大於 5M、比如 10M 的數據、 放不進去了、就會出現 FULL gc 、FULL gc 會把整個緩存全都收掉、瞬間緩存數據就沒了、然後把 10M 的數據放進去、這就是弱引用、可以理解爲這是一種服務降級、如果是強引用那就直接掛了
4、新建的對象也有可能直接進入老年代
  4.1、大對象,可通過啓動參數設置 - XX:PretenureSizeThreshold=1024(單位爲字節,默認爲 0、也就是說所有的默認都在新生代)來代表超過多大時就不再新生代分配,而是直接在老年代分配
  4.2、大的數組對象,切數組中無引用外部對象。
5、老年代大小無配置參數 

java 持久代(perm generation)

1、持久代 = 整個堆 - 年輕代大小 - 老年代大小
2、-XX:PermSize 最小 -XX:MaxPermSize 最大
設置持久代的大小,一般情況推薦把 - XX:PermSize 設置成 -XX:MaxPermSize 的值爲相同的值,因爲永久代大小的調整也會導致堆內存需要觸發 fgc。
3、存放 Class、Method 元信息,其大小與項目的規模、類、方法的數量有關。一般設置爲 128M 就足夠,設置原則是預留 30% 的空間
剛開始設置了 128M、隨着程序的運行、java 有一個叫 lib 的地方放了很多類庫、這個類庫並不是所有的都加載的、只有在用的時候或者系統初始化的時候會加載一部分、比如已經佔了 100M 了、但是隨着業務的運行會動態去類
庫里加、把一些 Class 文件通過反射的方式裝進去、這樣你的內存不斷增大、達到 128M 以後就掛了、就會報方法區溢出、怎麼做?調大到 256M、然後監控、超過閾值再調大、簡單方式是調大、另外 JDK 裏邊有一個 GC 可以回收
如果能接受停機、就調大,簡單、快速、已解決問題爲主
4、永久代的回收方式
4.1、常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收
比如一個常量 = 5 它的意義就是個值、如果回收、發現它沒被引用就被回收了
4.2、對於無用的類進行回收,必須保證 3 點:
類跟常量不一樣、一個類裏邊可能有好多東西、比如這個類引用那個類、

jvm 垃圾收集算法

1、引用計數算法
每個對象有一個引用計數屬性,新增一個引用時計數加 1,引用釋放時計數減 1,計數爲 0 時可以回收。此方法簡單,無法解決對象相互循環引用的問題。還有一個問題是如何解決精準計數。
這種方法現在已經不用了 
2、根搜索算法
從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。不可達對象。

在 java 語言中,GC Roots 包括:
虛擬機棧中引用的對象。
方法區中類靜態屬性實體引用的對象。
方法區中常量引用的對象。
本地方法棧中 JNI 引用的對象。

jvm 垃圾回收算法

1、複製算法(Copying)

 從根集合掃描、就是剛纔說的 GC-Roots 收集算法、從它開始查你的引用、如果沒有被引用、開始執行算法、並將存活對象複製到一塊新的、(S0 或者 S1) 

2、標記清除算法

適合老生代去回收

標記 - 整理算法採用標記 - 清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象佔用的空間後,會將所有的存活對象往左端空閒空間移動,並更新對應的指針。
標記 - 整理算法是在標記清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。

名詞解釋

1、串行回收
gc 單線程內存回收、會暫停使有用戶線程
2、並行回收
收集是指多個 GC 線程並行工作,但此時用戶線程是暫停的;所以,Seral 是串行的,Paralle 收集器是並行的,而 CMS 收集器是併發的。
3、併發回收
是指用戶線程與 GC 線程同時執行(不一定是並行,可能交替,但總體上是在同時執行的),不需要停頓用戶線程(其實在 CMS 中用戶線程還是需要停頓的,只是非常短,GC 線程在另一個 CPU 上執行)

串行回收要區分好並行回收和併發回收的區別,這地方非常關鍵、在選擇 GC 的過程中根據應用場景來選擇

JVM 常見垃圾回收器

上圖是 HotSpot 裏的收集器,中間的橫線表示分代,有連線表示可以組合使用。

年輕代區域有
Serial 串行 
ParNew 併發 
Parallel Scavenge 並行
年老代區域有
CMS
Serial Old 
Parallel Old
G1 目前還不成熟 、適合年輕代和年老代

Serial 回收器(串行回收器)

是一個單線程的收集器,只能使用一個 CPU 或一條線程區完成垃圾收集;在進行垃圾收集時,必須暫停所有其它工作線程,直到收集完成。
缺點:Stop-The-World
優勢:簡單。對於單 CPU 的情況,由於沒有多線程交互開銷,反而可以更高效。是 Client 模式下默認的新生代收集器。

新生代 Serial 回收器
1、通過 - XX:+UseSerialGC 來開啓
Serial New+Serial Old 的收集器組合進行內存回收
2、使用複製算法。
3、獨佔式的垃圾回收。
一個線程進行 GC,串行。其它工作線程暫停。
老年代 Serial 回收器
1、-XX:UseSerialGC 來開啓
Serial New+Serial Old 的收集器組合進行內存回收
2、使用標記 - 壓縮算法
3、串行的、獨佔式的垃圾回收器。
因爲內存比較大的原因,回收比新生代慢

ParNew 回收器(並行回收器)

並行回收器也是獨佔式的回收器,在收集過程中,應用程序會全部暫停。但由於並行回收器使用多線程進行垃圾回收,因此,在併發能力比較強的 CPU 上,它產生的停頓時間要短
於串行回收器,而在單 CPU 或者併發能力較弱的系統中,並行回收器的效果不會比串行回收器好,由於多線程的壓力,它的實際表現很可能比串行回收器差。

新生代 ParNew 回收器

1、-XX:+UseParNewGC 開啓
新生代使用並行回收收集器,老年代使用串行收集器
2、-XX:ParallelGCThreads 指定線程數
默認最好與 CPU 數理相當,避免過多的線程數影響垃圾收集性能
3、使用複製算法。
4、並行的、獨佔式的垃圾回收器。

新生代 Parallel Scavenge 回收器

1、吞吐量優先回收器
關注 CPU 吞吐量,即運行用戶代碼的時間 / 總時間。比如:JVM 運行 100 分鐘,其中運行用戶代碼 99 分鐘,垃圾回收 1 分鐘。則吞吐量是 99%,這種收集器能最高效率的利用 CPU,適合運行後臺運算
2、-XX:+UseParallelGC 開啓
使用 Parallel Scavenge+Serial Old 收集器組合回收垃圾,這也是 Server 模式下的默認值
3、-XX:GCTimeRation
來設置用戶執行時間佔總時間的比例,默認 99,即 1% 的時間用來進行垃圾回收
4、-XX:MaxGCPauseMillis
設置 GC 的最大停頓時間
5、使用複製算法

老生代 Parallel Old 回收器

1、-XX:+UseParallelOldGC 開啓
使用 Parallel Scavenge +Parallel Old 組合收集器進行收集
2、使用標記整理算法。
3、並行的、獨佔式的垃圾回收器。

CMS(併發標記清除) 回收器

 

運作過程分爲 4 個階段:
初始標記(CMS inital mark):值標記 GC Roots 能直接關聯到的對象。
併發標記(CMS concurrent mark):進行 GC RootsTracing 的過程。
重新標記(CMS remark):修正併發標記期間用戶程序繼續運行而導致標記發生改變的那一部分對象的標記.

併發清除(CMS concurrent sweep):
其中標記和重新標記兩個階段仍然需要 Stop-The-World, 整個過程中耗時最長的併發標記和併發清除過程中收集器都可以和用戶線程一起工作

CMS(併發標記清除)回收器

1、標記 - 清除算法
同時它又是一個使用多線程併發回收的垃圾收集器
2、-XX:ParalleCMSThreads
手工設定 CMS 的線程數量,CMS 默認啓動的線程數是(ParallelGCTherads+3)+3/4)
這是它的公式,一般情況下、對於 IO 密集型的 cpu 的核數乘以 2+1 ,CPU 密集型的一般 CPU 的核數 + 1
3、-XX+UseConcMarkSweepGC 開啓
使用 ParNew+CMS+Serial Old 的收集器組合進行內存回收,Serial Old 作爲 CMS 出現 “Concurrent Mode Failure” 失敗後的後備收集器使用.

失敗以後就會觸發 Full GC 、位了避免這種情況發生、就要對它進行配置、觸發 Full GC 有兩種情況、promotion failed 和 concurrent mode failure
對於採用 CMS 進行老年代 GC 的程序而言,尤其要注意 GC 日誌中是否有 promotion failed 和 concurrent mode failure 兩種狀況,當這兩種狀況出現時可能
會觸發 Full GC。
promotion failed 是在進行 Minor GC 時,survivor space 放不下、對象只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure 是在
執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候 “空間不足” 是 CMS GC 時當前的浮動垃圾過多導致暫時性的空間不足觸發 Full GC)。
對應措施爲:增大 survivor space、老年代空間或調低觸發併發 GC 的比率。

4、-XX:CMSInitiatingOccupancyFraction
設置 CMS 收集器在老年代空間被使用多少後觸發垃圾回收器,默認值爲 68%, 僅在 CMS 收集器時有效,-XX:CMSInitiatingOccupancyFraction=70

(一般情況爲 70%,設太高了可能會出現失敗,設太低了、頻繁, 只能去找一個比值、可以分析 GC log、看是否符合你的要求 )

5、-XX:+UseCMSCompactAtFullCollection 
由於 CMS 收集器會產生碎片,此參數設置在垃圾收集器後是否需要一次內存碎片整理過程,僅在 CMS 收集器時有效

6、-XX:+CMSFullGCBeforeCompaction
設置 CMS 收集器在進行若干次垃圾收集後再進行一次內存碎片整理過程, 通常與 UseCMSCompactAtFullCollection 參數一起使用
7、-XX:CMSInitiatingPermOccupancyFraction
設置 Perm Gen 使用到達多少比率時觸發, 默認 92%

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jYXcNijQDMpUKTvoc_IxMA