一篇與面試官和善交流的深刻了解JVM(JDK8)

文章目錄

 

一、類加載機制

類加載過程分爲 加載 >> 驗證 >> 準備 >> 解析 >> 初始化 >> 使用 >> 卸載 

一、加載
	在硬盤上查找並經過IO讀入字節碼文件,使用到類時纔會加載,例如調用類的main()方法,new對象 等等,在加載階段會在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口 

二、驗證 
	校驗字節碼文件的正確性 
    
三、準備
	給類的靜態變量分配內存,並賦予默認值 

四、解析 
	將符號引用替換爲直接引用,該階段會把一些靜態方法(符號引用,好比main()方法)替換爲指向數據 所存內存的指針或句柄等(直接引用),這是所謂的靜態連接過程(類加載期間完成),動態連接是在程 序運行期間完成的將符號引用替換爲直接引用,下節課會講到動態連接 

五、初始化 
	對類的靜態變量初始化爲指定的值,執行靜態代碼塊
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

二、雙親委派機制(先找父親加載,不行再由兒子本身加載)

2.一、類加載器java

一、根類加載器(**Bootstrap classLoader**):負責加載支撐JVM運行的位於JRE的lib目錄下的核心類庫,好比rt.jar、charsets.jar等
    
二、擴展類加載器(**ExtClassLoader**):負責加載支撐JVM運行的位於JRE的lib目錄下的ext擴展目錄中的JAR類包
    
三、應用加載器(**AppClassLoader**):負責加載ClassPath路徑下的類包,主要就是加載你本身寫的那些類,負責加載用戶自定義路徑下的類包
  • 1
  • 2
  • 3
  • 4
  • 5

2.二、加載器初始化過程程序員

類運行加載全過程會建立JVM啓動器實例sun.misc.Launcher。sun.misc.Launcher初始化使用了單例模式設計,保證一個JVM虛擬機內只有一個sun.misc.Launcher實例。在Launcher構造方法內部,其建立了兩個類加載器,分別是sun.misc.Launcher.ExtClassLoader(擴展類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。
    
JVM默認使用launcher的`getClassLoader()`方法返回的類加載器`AppClassLoader`的實例來加載咱們的應用程序。
  • 1
  • 2
  • 3

2.三、雙親委派機制web

應用程序類加載器AppClassLoader加載類的雙親委派機制源碼,AppClassLoader的loadClass方法最終會調用其父類ClassLoader的loadClass方法,該方法的大致邏輯以下:
首先,檢查一下指定名稱的類是否已經加載過,若是加載過了,就不須要再加載,直接返回。
若是此類沒有加載過,那麼,再判斷一下是否有父加載器;若是有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用bootstrap類加載器來加載。
若是父加載器及bootstrap類加載器都沒有找到指定的類,那麼調用當前類加載器的findClass方法來完成類加載。算法

2.四、爲何要設計雙親委派機制?spring

一、沙箱安全機制:本身寫的java.lang.String.class類不會被加載,這樣即可以防止核心API庫被隨意篡改
二、避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的惟一性數據庫

2.五、全盤負責委託機制bootstrap

「全盤負責」是指當一個ClassLoder裝載一個類時,除非顯示的使用另一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入數組

2.六、自定義類加載器示例瀏覽器

自定義類加載器只須要繼承 java.lang.ClassLoader 類,該類有兩個核心方法,一個是loadClass(String, boolean),實現了雙親委派機制,還有一個方法是findClass,默認實現是空方法,因此咱們自定義類加載器主要是重寫findClass方法。緩存

三、tomcat怎麼破解類加載機制

一、commonLoader:Tomcat最基本的類加載器,加載路徑中的class能夠被Tomcat容器自己以及各個Webapp訪問;

二、catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對於Webapp不可見;

三、sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對於全部Webapp可見,可是對於Tomcat容器不可見;

四、WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見,好比加載war包裏相關的類, 每一個war包應用都有本身的WebappClassLoader,實現相互隔離,好比不一樣war包應用引入了不一樣的spring版本,這樣實現就能加載各自的spring版本;

五、模擬實現Tomcat的JasperLoader熱加載

​ 原理:後臺啓動線程監聽jsp文件變化,若是變化了找到該jsp對應的servlet類的加載器引用 (gcroot),從新生成新的JasperLoader加載器賦值給引用,而後加載新的jsp對應的servlet類,以前的那個加載器由於沒有gcroot引用了,下一次gc的時候會被銷燬

=>總結:每一個webappClassLoader加載本身的目錄下的class文件,不會傳遞給父類加載器,打破了雙親委派機制。

四、內存模型

4.一、線程私有區域

程序計數器:是當前線程所執行的字節碼的行號指示器,無OOM

虛擬機棧:是描述java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。

  • 棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態連接

(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨着方法調用而創

建,隨着方法結束而銷燬——不管方法是正常完成仍是異常完成(拋出了在方法內未被捕獲的異

常)都算做方法結束。

本地方法棧:和 Java Stack 做用相似, 區別是虛擬機棧爲執行 Java 方法服務, 而本地方法棧則爲

Native 方法服務, 若是一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個

C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一。

4.二、線程共享區域

==堆-運行時數據區:==是被線程共享的一塊內存區域,建立的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域。因爲現代 VM 採用分代收集算法, 所以 Java 堆從 GC 的角度還能夠細分爲: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代

方法區/永久代(1.8以後元空間):用於存儲被 JVM 加載的類信息**、常量靜態變量、**即時編譯器編譯後的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即便用Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就能夠像管理 Java 堆同樣管理這部份內存, 而沒必要爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 所以收益通常很小)。

  • 運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。

在這裏插入圖片描述

直接內存

jdk1.4後加入NIO(New Input/Output)類,引入了一種基於通道與緩衝區的I/O方式,它能夠使用native函數庫直接分配堆外內存,而後經過一個存儲在java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。能夠避免在Java堆和Native堆中來回複製數據

直接內存的分配不會受到Java堆大小的限制.避免大於物理內存的狀況

五、對象的建立

在這裏插入圖片描述

一、類加載檢查

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
new指令對應到語言層面上講是,new關鍵詞、對象克隆、對象序列化等
  • 1
  • 2

二、分配內存

在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類 加載完成後即可徹底肯定,爲對象分配空間的任務等同於把 一塊肯定大小的內存從Java堆中劃分出來。
 //如何劃份內存?
 一、「指針碰撞」(Bump the Pointer)(默認用指針碰撞)
		若是Java堆中內存是絕對規整的,全部用過的內存都放在一邊,空閒的內存放在另外一邊,中	  間放着一個指針做爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段	 與對象大小相等的距離。
 二、「空閒列表」(Free List)
		若是Java堆中的內存並非規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡	  單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從	 列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。
 //解決併發問題的方法
 一、CAS(compare and swap)
		虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性來對分配內存空間的動做進行同	 步處理。
	二、本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)
		把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一	  小塊內存。經過­XX:+/­UseTLAB參數來設定虛擬機是否使用TLAB(JVM會默認開啓			­XX:+UseTLAB),­XX:TLABSize指定TLAB大小。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.初始化

內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭), 若是使用TLAB,這一工做過程也能夠提早至TLAB分配時進行。這一步操做保證了對象的實例字段在Java代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
  • 1

4.設置對象頭

初始化零值以後,虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭Object Header之中。
在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、 實例數據(Instance Data)和對齊填充(Padding)。 HotSpot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時 間戳等。對象頭的另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
  • 1
  • 2

在這裏插入圖片描述

5.執行方法

執行<init>方法,即對象按照程序員的意願進行初始化。對應到語言層面上講,就是爲屬性賦值(注意,這與上面的賦零值不一樣,這是由程序員賦的值),和執行構造方法。
  • 1

5.一、對象大小與指針壓縮

5.1.一、對象大小

對象大小能夠用 jol­-core 包查看

5.1.二、什麼是java對象的指針壓縮?

  1. jdk1.6 update14開始,在64bit操做系統中,JVM支持指針壓縮
  2. jvm配置參數:UseCompressedOops,compressed­­壓縮、oop(ordinary object pointer)­­對象指針
  3. 啓用指針壓縮:­XX:+UseCompressedOops(默認開啓),禁止指針壓縮:­XX:­UseCompressedOops

5.1.三、爲何要進行指針壓縮?

1.在64位平臺的HotSpot中使用32位指針,內存使用會多出1.5倍左右,使用較大指針在主內存和緩存之間移動數據,佔用較大寬帶,同時GC也會承受較大壓力
2.爲了減小64位平臺下內存的消耗,啓用指針壓縮功能
3.在jvm中,32位地址最大支持4G內存(2的32次方),能夠經過對對象指針的壓縮編碼、解碼方式進行優化,使得jvm
只用32位地址就能夠支持更大的內存配置(小於等於32G)
4.堆內存小於4G時,不須要啓用指針壓縮,jvm會直接去除高32位地址,即便用低虛擬地址空間
5.堆內存大於32G時,壓縮指針會失效,會強制使用64位(即8字節)來對java對象尋址,這就會出現1的問題,因此堆內存不要大於32G爲好

六、對象的分配過程

在這裏插入圖片描述

6.一、棧上分配

咱們經過JVM內存分配能夠知道JAVA中的對象都是在堆上進行分配,當對象沒有被引用的時候,須要依靠GC進行回收內存,若是對象數量較多的時候,會給GC帶來較大壓力,也間接影響了應用的性能。爲了減小臨時對象在堆內分配的數量,JVM經過逃逸分析肯定該對象不會被外部訪問。若是不會逃逸能夠將該對象在棧上分配內存,這樣該對象所佔用的內存空間就能夠隨棧幀出棧而銷燬,就減輕了垃圾回收的壓力。

==對象逃逸分析:==就是分析對象動態做用域,當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞到其餘地方中。

public User test1() {
  User user = new User();
  user.setId(1);
  user.setName("zhuge");
  //TODO 保存到數據庫
  return user;
 }

 public void test2() {
  User user = new User();
  user.setId(1);
  user.setName("zhuge");
  //TODO 保存到數據庫
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

很顯然test1方法中的user對象被返回了,這個對象的做用域範圍不肯定,test2方法中的user對象咱們能夠肯定當方法結束這個對象就能夠認爲是無效對象了,對於這樣的對象咱們其實能夠將其分配在棧內存裏,讓其在方法結束時跟隨棧內存一塊兒被回收掉。
JVM對於這種狀況能夠經過開啓逃逸分析參數(-XX:+DoEscapeAnalysis)來優化對象內存分配位置,使其經過標量替換優先分配在棧上(棧上分配),JDK7以後默認開啓逃逸分析,若是要關閉使用參數(-XX:-DoEscapeAnalysis)
==標量替換:==經過逃逸分析肯定該對象不會被外部訪問,而且對象能夠被進一步分解時,JVM不會建立該對象,而是將該對象成員變量分解若干個被這個方法使用的成員變量所代替,這些代替的成員變量在棧幀或寄存器上分配空間,這樣就不會由於沒有一大塊連續空間致使對象內存不夠分配。開啓標量替換參數(-XX:+EliminateAllocations),JDK7以後默認開啓。
==標量與聚合量:==標量即不可被進一步分解的量,而JAVA的基本數據類型就是標量(如:int,long等基本數據類型以及reference類型等),標量的對立就是能夠被進一步分解的量,而這種量稱之爲聚合量。而在JAVA中對象就是能夠被進一步分解的聚合量。

結論:棧上分配依賴於逃逸分析和標量替換

6.二、對象在Eden區分配(大部分狀況,當 Eden 區沒有足夠空間進行分配時,出現Young GC)

大量的對象被分配在eden區,eden區滿了後會觸發Young GC,可能會有99%以上的對象成爲垃圾被回收掉,剩餘存活的對象會被挪到s0區,下一次eden區滿了後又會觸發Young GC,把eden區和s0區垃圾對象回收,把剩餘存活的對象一次性挪動到另一塊爲空的s1區,由於新生代的對象都是朝生夕死的,存活時間很短,因此JVM默認的8:1:1的比例是很合適的,讓eden區儘可能的大,survivor區夠用便可,JVM默認有這個參數-XX:+UseAdaptiveSizePolicy(默認開啓),會致使這個8:1:1比例自動變化,若是不想這個比例有變化能夠設置參數-XX:-UseAdaptiveSizePolicy

6.三、大對象直接進入老年代

大對象就是須要大量連續內存空間的對象(好比:字符串、數組)。JVM參數 -XX:PretenureSizeThreshold 能夠設置大對象的大小,若是對象超過設置大小會直接進入老年代,不會進入年輕代,這個參數只在 Serial 和ParNew兩個收集器下有效。
好比設置JVM參數:-XX:PretenureSizeThreshold=1000000 (單位是字節) -XX:+UseSerialGC ,再執行下上面的第一個程序會發現大對象直接進了老年代

爲何要這樣呢?
爲了不爲大對象分配內存時的複製操做而下降效率。

6.四、長期存活的對象將進入老年代

虛擬機給每一個對象一個對象年齡(Age)計數器。若是對象在 Eden 出生並通過第一次 Minor GC 後仍然可以存活,而且能被 Survivor 容納的話,將被移動到 Survivor
空間中,並將對象年齡設爲1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲,CMS收集器默認6歲,不一樣的垃圾收集器會略微有點不一樣),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 來設置。

6.五、對象動態年齡判斷

當前放對象的Survivor區域裏(其中一塊區域,放對象的那塊s區),一批對象的總大小大於這塊Survivor區域內存大小的50%(-XX:TargetSurvivorRatio能夠指定),那麼此時大於等於這批對象年齡最大值的對象,就能夠直接進入老年代了,例如Survivor區域裏如今有一批對象,年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代。這個規則實際上是但願那些多是長期存活的對象,儘早進入老年代。對象動態年齡判斷機制通常是在young gc以後觸發的。

6.六、老年代空間分配擔保機制)

年輕代每次minor gc以前JVM都會計算下老年代剩餘可用空間
若是這個可用空間小於年輕代裏現有的全部對象大小之和(包括垃圾對象)
就會看一個「-XX:-HandlePromotionFailure」(jdk1.8默認就設置了) 的參數是否設置了
若是有這個參數,就會看看老年代的可用內存大小,是否大於以前每一次minor gc後進入老年代的對象的平均大小。
若是上一步結果是小於或者以前說的參數沒有設置,那麼就會觸發一次Full gc,對老年代和年輕代一塊兒回收一次垃圾,若是回收完仍是沒有足夠空間存放新的對象就會發生"OOM"。

固然,若是minor gc以後剩餘存活的須要挪動到老年代的對象大小仍是大於老年代可用空間,那麼也會觸發full gc,full gc完以後若是仍是沒有空間放minor gc以後的存活對象,則也會發生「OOM」

在這裏插入圖片描述

七、如何判斷一個類是無用的類

  • 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

八、finalize()方法最終斷定對象是否存活

1. 第一次標記並進行一次篩選。

篩選的條件是此對象是否有必要執行finalize()方法。

當對象沒有覆蓋finalize方法,對象將直接被回收。

2. 第二次標記

若是這個對象覆蓋了finalize方法,finalize方法是對象脫逃死亡命運的最後一次機會,若是對象要在finalize()中成功拯救 本身,只要從新與引用鏈上的任何的一個對象創建關聯便可,譬如把本身賦值給某個類變量或對象的成員變量,那在第 二次標記時它將移除出「即將回收」的集合。若是對象這時候還沒逃脫,那基本上它就真的被回收了。

注意:一個對象的finalize()方法只會被執行一次,也就是說經過調用finalize方法自我救命的機會就一次。

九、常見引用類型(四大引用)

一、強引用:普通的變量引用

二、軟引用(SoftReference):將對象用SoftReference軟引用類型的對象包裹,正常狀況不會被回收,可是GC作完後發現釋放不出空間存放新的對象,則會把這些軟引用的對象回收掉。軟引用可用來實現內存敏感的高速緩存。

  • 使用場景:瀏覽器的後退按鈕

三、弱引用(WeakReference):將對象用WeakReference軟引用類型的對象包裹,弱引用跟沒引用差很少,GC會直接回收掉,不多用

四、虛引用:虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係,幾乎不用

十、對象回收

什麼叫對象回收?

堆中幾乎放着全部的對象實例,對堆垃圾回收前的第一步就是要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)。

10.一、引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任什麼時候候計數器爲0的對象就是不可能再被使用的。
缺點:循環引用問題

10.二、可達性分析算法(gcroot)

將「GC Roots」 對象做爲起點,從這些節點開始向下搜索引用的對象,找到的對象都標記爲非垃圾對象,其他未標記的對象都是垃圾對象
GC Roots根節點:線程棧的本地變量、靜態變量、本地方法棧的變量等等

在這裏插入圖片描述

十一、四大垃圾回收算法

11.一、分代收集理論

當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存分爲幾塊。通常將java堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。
好比在新生代中,每次收集都會有大量對象(近99%)死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇「標記-清除」或「標記-整理」算法進行垃圾收集。注意,「標記-清除」或「標記-整理」算法會比複製算法慢10倍以上。

11.二、標記-複製算法

它能夠將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的
內存使用完後,就將還存活的對象複製到另外一塊去,而後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

在這裏插入圖片描述

11.三、標記-清除算法

算法分爲「標記」和「清除」階段:標記存活的對象, 統一回收全部未被標記的對象(通常選擇這種);也能夠反過來,標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象 。它是最基礎的收集算法,比較簡單,可是會帶來兩個明顯的問題:

  1. 效率問題 (若是須要標記的對象太多,效率不高)
  2. 空間問題(標記清除後會產生大量不連續的碎片)

在這裏插入圖片描述

11.四、標記-整理算法

根據老年代的特色特出的一種標記算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。

在這裏插入圖片描述

十二、常見oom

一、java.lang.StackOverflowError:

​ 報這個錯誤通常是因爲方法深層次的調用,默認的線程棧空間大小通常與具體的硬件平臺有關。棧內存爲線程私有的空間,每一個線程都會建立私有的棧內存。棧空間內存設置過大,建立線程數量較多時會出現棧內存溢出StackOverflowError。同時,棧內存也決定方法調用的深度,棧內存太小則會致使方法調用的深度較小,如遞歸調用的次數較少。

二、java.lang.OutOfMemoryError: Java heap space

​ Heap size 設置 JVM堆的設置是指:java程序執行過程當中JVM可以調配使用的內存空間的設置。JVM在啓動的時候會本身主動設置Heap size的值,其初始空間(即-Xms)是物理內存的1/64最大空間(-Xmx)是物理內存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等選項可進行設置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。

三、java.lang.OutOfMemoryError:GC overhead limit exceeded

​ GC回收時間過長時會拋出的OutOfMemory。過長是指,超過98%的時間都在用來作GC而且回收了不到2%的堆內存。連續屢次的GC,都回收了不到2%的極端狀況下才會拋出。假如不拋出GC overhead limit 錯誤會發生什麼事情呢?那就是GC清理出來的一點內存很快又會被再次填滿,強迫GC再次執行,這樣形成惡性循環,CPU的使用率一直很高,可是GC沒有任何的進展。

四、java.lang.OutOfMemoryError:Direct buffer memory

​ 寫NIO程序常用到ByteBuffer來讀取或者寫入數據,這是一種基於通道與緩衝區的I/O方式。它能夠使用Native函數庫直接分配堆外內存,而後經過一個存儲在java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中提升性能,由於避免了java堆和Native堆中來回複製數據。

  • ByteBuffer.allocate(capability) :這種方式是分配JVM堆內存,屬於GC管轄範圍以內。因爲須要拷貝,因此速度相對較慢;
  • ByteBuffer.allocateDirect(capability):這種方式是直接分配OS本地內存,不屬於GC管轄範圍以內,因爲不須要內存拷貝因此速度相對較快。

可是若是不斷分配本地內存,堆內存不多使用,那麼JVM就不須要執行GC,DirectByteBuffer對象就不會被回收。這時候堆內存充足,可是本地內存已經用光了,再次嘗試分配的時候就會出現OutOfMemoryError,那麼程序就直接崩潰了。

五、java.lang.OutOfMemoryError:unable to create new native thread

​ 準確的說,這一個異常是和程序運行的平臺相關的。致使的緣由:

  • 建立了太多的線程,一個應用建立多個線程,超過系統承載極限;
  • 服務器不容許應用程序建立這麼多的線程,Linux系統默認的容許單個進程能夠建立的線程數量是1024個,當建立多 線程數量多於這個數字的時候就會拋出此異常

如何解決呢?

  • 想辦法減小應用程序建立的線程的數量,分析應用是否真的須要建立這麼多的線程。若是不是,改變代碼將線程數量降到最低;
  • 對於有的應用,確實須要建立不少的線程,遠超過Linux限制的1024個 限制,那麼能夠經過修改Linux服務器的配置,擴大Linux的默認限制。

六、java.lang.OutOfMemoryError:MetaSpace

​ 元空間的本質和永久代相似,都是對JVM規範中的方法區的實現。不過元空間與永久代之間最大的區別在於:元空間不在虛擬機中,而是使用的本地內存。所以,默認狀況下,元空間的大小僅僅受到本地內存的限制 。

元空間存放了如下的內容:

  • 虛擬機加載的類信息;
  • 常量池;
  • 靜態變量;
  • 即時編譯後的代碼

模擬MetaSpace空間溢出,咱們不斷生成類往元空間裏灌,類佔據的空間老是會超過MetaSpace指定的空間大小的

查看元空間的大小:java -XX:+PrintFlagsInitial

1三、垃圾收集器

13.一、CMS(「標記-清除」算法, -XX:+UseConcMarkSweepGC(old)

定義:以獲取最短回收停頓時間爲目標的收集器

13.1.一、運做過程(5大步驟)

一、初始標記: 暫停全部的其餘線程(STW),並記錄下gc roots直接能引用的對象,速度很快。
二、併發標記: 併發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程, 這個過程耗時較長可是不須要停頓用戶線程, 能夠與垃圾收集線程一塊兒併發運行。由於用戶程序繼續運行,可能會有致使已經標記過的對象狀態發生改變。
三、從新標記: 從新標記階段就是爲了修正併發標記期間由於用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段的時間稍長,遠遠比並發標記階段時間短。主要用到增量更新算法作從新標記。
四、併發清理: 開啓用戶線程,同時GC線程開始對未標記的區域作清掃。這個階段若是有新增對象會被標記爲三色標記法裏面的黑色不作任何處理
五、併發重置:重置本次GC過程當中的標記數據。

在這裏插入圖片描述

主要優勢:併發收集、低停頓。可是它有下面幾個明顯的缺點:

  1. 對CPU資源敏感(會和服務搶資源);
  2. 沒法處理浮動垃圾(在併發標記和併發清理階段又產生垃圾,這種浮動垃圾只能等到下一次gc再清理了);
  3. 它使用的回收算法-「標記-清除」算法會致使收集結束時會有大量空間碎片產生,固然經過參數==-XX:+UseCMSCompactAtFullCollection可讓jvm在執行完標記清除後再作整理==
  4. 執行過程當中的不肯定性,會存在上一次垃圾回收還沒執行完,而後垃圾回收又被觸發的狀況,特別是在併發標記和併發清理階段會出現,一邊回收,系統一邊運行,也許沒回收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收

CMS的相關核心參數

  1. -XX:+UseConcMarkSweepGC:啓用cms
  2. -XX:ConcGCThreads:併發的GC線程數
  3. -XX:+UseCMSCompactAtFullCollection:FullGC以後作壓縮整理(減小碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC以後壓縮一次,默認是0,表明每次FullGC後都會壓縮一次
  5. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(默認是92,這是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),若是不指定,JVM僅在第一次使用設定值,後續則會自動調整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啓動一次minor gc,目的在於減小老年代對年輕代的引用,下降CMS GC的標記階段時的開銷,通常CMS的GC耗時 80%都在標記階段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW
  9. -XX:+CMSParallelRemarkEnabled:在從新標記的時候多線程執行,縮短STW;

13.1.二、三色標記法

黑色: 表示對象已經被垃圾收集器訪問過, 且這個對象的全部引用都已經掃描過。 黑色的對象表明已經掃描過, 它是安全存活的, 若是有其餘對象引用指向了黑色對象, 無須從新掃描一遍。 黑色對象不可能直接(不通過灰色對象) 指向某個白色對象。

灰色: 表示對象已經被垃圾收集器訪問過, 但這個對象上至少存在一個引用尚未被掃描過。

白色: 表示對象還沒有被垃圾收集器訪問過。 顯然在可達性分析剛剛開始的階段, 全部的對象都是白色的, 若在分析結束的階段, 仍然是白色的對象, 即表明不可達。

在這裏插入圖片描述

13.1.三、concurrent model failure(浮動垃圾)

在併發標記過程當中,若是因爲方法運行結束致使部分局部變量(gcroot)被銷燬,這個gcroot引用的對象以前又被掃描過(被標記爲非垃圾對象),那麼本輪GC不會回收這部份內存。這部分本應該回收可是沒有回收到的內存,被稱之爲「浮動垃圾」。浮動垃圾並不會影響垃圾回收的正確性,只是須要等到下一輪垃圾回收中才被清除。另外,針對併發標記(還有併發清理)開始後產生的新對象,一般的作法是直接所有當成黑色,本輪不會進行清除。這部分對象期間可能也會變爲垃圾,這也算是浮動垃圾的一部分。
  • 1

13.1.四、background/foreground collector

-XX:ConcGCThreads=4和-XX:+ExplicitGCInvokesConcurrent開啓foreground CMS GC,CMS gc 有兩種模式,background和foreground,正常的cms gc使用background模式,就是咱們平時說的cms gc;當併發收集失敗或者調用了System.gc()的時候,就會致使一次full gc,這個fullgc是否是cms回收,而是Serial單線程回收器,加入了參數 -XX:ConcGCThreads=4 後,執行full gc的時候,就變成了CMS foreground gc,它是並行full gc,只會執行cms中stop the world階段的操做,效率比單線程Serial full GC要高;須要注意的是它只會回收old,由於cms收集器是老年代收集器;而正常的Serial收集是包含整個堆的,加入了參數==-XX:+ExplicitGCInvokesConcurrent==,表明永久帶也會被cms收集;

13.1.五、爲何G1用SATB?CMS用增量更新?

SATB相對增量更新效率會高(固然SATB可能形成更多的浮動垃圾),由於不須要在從新標記階段再次深度掃描被刪除引用對象,而CMS對增量引用的根對象會作深度掃描,G1由於不少對象都位於不一樣的region,CMS就一塊老年代區域,從新深度掃描對象的話G1的代價會比CMS高,因此G1選擇SATB不深度掃描對象,只是簡單標記,等到下一輪GC再深度掃描。

13.1.六、漏標-讀寫屏障(解決方案)

13.1.6.一、增量更新(Incremental Update)+寫屏障

增量更新就是當黑色對象插入新的指向白色對象的引用關係時, 就將這個新插入的引用記錄下來, 等併發掃描結束以後, 再將這些記錄過的引用關係中的黑色對象爲根, 從新掃描一次。 這能夠簡化理解爲, 黑色對象一旦新插入了指向白色對象的引用以後, 它就變回灰色對象了。

  • 寫屏障實現增量更新

當對象A的成員變量的引用發生變化時,好比新增引用(a.d = d),咱們能夠利用寫屏障,將A新的成員變量引用對象D
記錄下來:
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 記錄新引用的對象
}

13.1.6.二、原始快照(Snapshot At The Beginning,SATB)+寫屏障

原始快照就是當灰色對象要刪除指向白色對象的引用關係時, 就將這個要刪除的引用記錄下來, 在併發掃描結束以後,再將這些記錄過的引用關係中的灰色對象爲根, 從新掃描一次,這樣就能掃描到白色的對象,將白色對象直接標記爲黑色(目的就是讓這種對象在本輪gc清理中能存活下來,待下一輪gc的時候從新掃描,這個對象也有多是浮動垃圾)

以上不管是對引用關係記錄的插入仍是刪除, 虛擬機的記錄操做都是經過寫屏障實現的。

  • 寫屏障實現SATB

    當對象B的成員變量的引用發生變化時,好比引用消失(a.b.d = null),咱們能夠利用寫屏障,將B原來成員變量的引用
    對象D記錄下來:
    void pre_write_barrier(oop* field) {
    oop old_value = *field; // 獲取舊值
    remark_set.add(old_value); // 記錄原來的引用對象
    }

13.1.6.三、併發標記時對漏標的處理方案

CMS:寫屏障 + 增量更新
G1,Shenandoah:寫屏障 + SATB
ZGC:讀屏障

工程實現中,讀寫屏障還有其餘功能,好比寫屏障能夠用於記錄跨代/區引用的變化,讀屏障能夠用於支持移動對象的併發執行等。功能以外,還有性能的考慮,因此對於選擇哪一種,每款垃圾回收器都有本身的想法。

13.1.七、promotion failed

這個異常發生在年輕帶回收的時候;
在進行Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下形成的,多數是因爲老年帶有足夠的空閒空間,可是因爲碎片較多,新生代要轉移到老年帶的對象比較大,找不到一段連續區域存放這個對象致使的,

13.1.八、過早提高和提高失敗

在 Minor GC 過程當中,Survivor Unused 可能不足以容納 Eden 和另外一個 Survivor 中的存活對象, 那麼多餘的將被移到老年代, 稱爲過早提高(Premature Promotion),這會致使老年代中短時間存活對象的增加, 可能會引起嚴重的性能問題。 再進一步, 若是老年代滿了, Minor GC 後會進行 Full GC, 這將致使遍歷整個堆, 稱爲提高失敗(Promotion Failure)。

13.1.九、早提高的緣由

  1. Survivor空間過小,容納不下所有的運行時短生命週期的對象,若是是這個緣由,能夠嘗試將Survivor調大,不然端生命週期的對象提高過快,致使老年代很快就被佔滿,從而引發頻繁的full gc;
  2. 對象太大,Survivor和Eden沒有足夠大的空間來存放這些大象;

13.1.十、提高失敗緣由

當提高的時候,發現老年代也沒有足夠的連續空間來容納該對象。
爲何是沒有足夠的連續空間而不是空閒空間呢?
老年代容納不下提高的對象有兩種狀況:

  1. 老年代空閒空間不夠用了;

  2. 老年代雖然空閒空間不少,可是碎片太多,沒有連續的空閒空間存放該對象;

    解決方法

    1. 若是是由於內存碎片致使的大對象提高失敗,cms須要進行空間整理壓縮;
    2. 若是是由於提高過快致使的,說明Survivor 空閒空間不足,那麼能夠嘗試調大 Survivor;
    3. 若是是由於老年代空間不夠致使的,嘗試將CMS觸發的閾值調低

13.二、G1

定義:面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高機率知足GC,停頓時間要求的同時,還具有高吞吐量性能特徵

13.2.一、運做流程

G1將Java堆劃分爲多個大小相等的獨立區域(Region),JVM最多能夠有2048個Region。
通常Region大小等於堆大小除以2048,好比堆大小爲4096M,則Region大小爲2M,固然也能夠用參數"-XX:G1HeapRegionSize"手動指定Region大小,可是推薦默認的計算方式。
G1保留了年輕代和老年代的概念,但再也不是物理隔閡了,它們都是(能夠不連續)Region的集合。
默認年輕代對堆內存的佔比是5%,若是堆大小爲4096M,那麼年輕代佔據200MB左右的內存,對應大概是100個Region,能夠經過「-XX:G1NewSizePercent」設置新生代初始佔比,在系統運行中,JVM會不停的給年輕代增長更多的Region,可是最多新生代的佔比不會超過60%,能夠經過「-XX:G1MaxNewSizePercent」調整。年輕代中的Eden和Survivor對應的region也跟以前同樣,默認8:1:1,假設年輕代如今有1000個region,eden區對應800個,s0對應100個,s1對應100個。
一個Region可能以前是年輕代,若是Region進行了垃圾回收,以後可能又會變成老年代,也就是說Region的區域功能
可能會動態變化。G1垃圾收集器對於對象何時會轉移到老年代跟以前講過的原則同樣,惟一不一樣的是對大對象的處理,G1有專門分配大對象的Region叫Humongous區,而不是讓大對象直接進入老年代的Region中。在G1中,大對象的斷定規則就是一個大對象超過了一個Region大小的50%,好比按照上面算的,每一個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,並且一個大對象若是太大,可能會橫跨多個Region來存放。

Humongous區專門存放短時間巨型對象,不用直接進老年代,能夠節約老年代的空間,避免由於老年代空間不夠的GC開銷。
Full GC的時候除了收集年輕代和老年代以外,也會將Humongous區一併回收。

G1收集器一次GC的運做過程大體分爲如下4個步驟:

  • 初始標記(initial mark,STW):暫停全部的其餘線程,並記錄下gc roots直接能引用的對象,速度很快 ;

  • 併發標記(Concurrent Marking):同CMS的併發標記

  • 最終標記(Remark,STW):同CMS的從新標記

  • 篩選回收(Cleanup,STW):篩選回收階段首先對各個Region的==回收價值和成本進行排序,根據用戶所指望的GC停頓時間(能夠用JVM參數 -XX:MaxGCPauseMillis指定)來制定回收計劃,==好比說老年代此時有1000個Region都滿了,可是由於根據預期停頓時間,本次垃圾回收可能只能停頓200毫秒,那麼經過以前回收成本計算得知,可能回收其中800個Region恰好須要200ms,那麼就只會回收800個Region(Collection Set,要回收的集合),儘可能把GC致使的停頓時間控制在咱們指定的範圍內。這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。無論是年輕代或是老年代,回收算法主要用的是複製算法,將一個region中的存活對象複製到另外一個region中,這種不會像CMS那樣回收完由於有不少內存碎片還須要整理一次,G1採用複製算法回收幾乎不會有太多內存碎片。(注意:CMS回收階段是跟用戶線程一塊兒併發執行的,G1由於內部實現太複雜暫時沒實現併發回收,不過到了Shenandoah就實現了併發收集,Shenandoah能夠當作是G1的升級版本)

在這裏插入圖片描述

==G1收集器在後臺維護了一個優先列表,每次根據容許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來),好比一個Region花200ms能回收10M垃圾,另一個Region花50ms能回收20M垃圾,在回收時間有限狀況下,G1固然會優先選擇後面這個Region回收。==這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限時間內能夠儘量高的收集效率。

被視爲JDK1.7以上版本Java虛擬機的一個重要進化特徵。它具有如下特色:

  • 並行與併發:G1能充分利用CPU、多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其餘收集器本來須要停頓Java線程來執行GC動做,G1收集器仍然能夠經過併發的方式讓java程序繼續執行。
  • 分代收集:雖然G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,可是仍是保留了分代的概念。
  • 空間整合:與CMS的「標記–清理」算法不一樣,G1從總體來看是基於「標記整理」算法實現的收集器;從局部上來看是基於「複製」算法實現的。
    可預測的停頓:這是G1相對於CMS的另外一個大優點,下降停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷(經過參數"-XX:MaxGCPauseMillis"指定)內完成垃圾收集。

毫無疑問, 能夠由用戶指按期望的停頓時間是G1收集器很強大的一個功能, 設置不一樣的指望停頓時間, 可以使得G1在不一樣應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這裏設置的「指望值」必須是符合實際的, 不能異想天開, 畢竟G1是要凍結用戶線程來複制對象的, 這個停頓時間再怎麼低也得有個限度。 它默認的停頓目標爲兩百毫秒, 通常來講, 回收階段佔到幾十到一百甚至接近兩百毫秒都很正常, 但若是咱們把停頓時間調得很是低, 譬如設置爲二十毫秒, 極可能出現的結果就是因爲停頓目標時間過短, 致使每次選出來的回收集只佔堆內存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 致使垃圾慢慢堆積。 極可能一開始收集器還能從空閒的堆內存中得到一些喘息的時間, 但應用運行時間一長就不行了, 最終佔滿堆引起Full GC反而下降性能, 因此一般把指望停頓時間設置爲一兩百毫秒或者兩三百毫秒會是比較合理的。

在這裏插入圖片描述

13.2.二、Remembered Set(記錄集)/Card Table(卡表)

在新生代作GCRoots可達性掃描過程當中可能會碰到跨代引用的對象,這種若是又去對老年代再去掃描效率過低了。
爲此,在新生代能夠引入記錄集(Remember Set)的數據結構(記錄從非收集區到收集區的指針集合),避免把整個老年代加入GCRoots掃描範圍。事實上並不僅是新生代、 老年代之間纔有跨代引用的問題, 全部涉及部分區域收集(Partial GC) 行爲的垃圾收集器, 典型的如G一、 ZGC和Shenandoah收集器, 都會面臨相同的問題。
垃圾收集場景中,收集器只需經過記憶集判斷出某一塊非收集區域是否存在指向收集區域的指針便可,無需瞭解跨代引用指針的所有細節。
hotspot使用一種叫作「卡表」(cardtable)的方式實現記憶集,也是目前最經常使用的一種方式。關於卡表與記憶集的關係,能夠類比爲Java語言中HashMap與Map的關係。
卡表是使用一個字節數組實現:CARD_TABLE[ ],每一個元素對應着其標識的內存區域一塊特定大小的內存塊,稱爲「卡頁」。
HotSpot使用的卡頁是2^9大小,即512字節。

一個卡頁中可包含多個對象,只要有一個對象的字段存在跨代指針,其對應的卡表的元素標識就變成1,表示該元素變髒,不然爲0。
GC時,只要篩選本收集區的卡表中變髒的元素加入GCRoots裏。

卡表如何維護?

卡表變髒上面已經說了,可是須要知道如何讓卡表變髒,即發生引用字段賦值時,如何更新卡表對應的標識爲1。Hotspot使用寫屏障維護卡表狀態。

13.2.三、Collect Set

Collect Set(CSet)是指,在Evacuation階段,由G1垃圾回收器選擇的待回收的Region集合。G1垃圾回收器的軟實時的特性就是經過CSet的選擇來實現的。對應於算法的兩種模式fully-young generational mode和partially-young mode,CSet的選擇能夠分紅兩種:

  1. 在fully-young generational mode下:顧名思義,該模式下CSet將只包含young的Region。G1將調整young的Region的數量來匹配軟實時的目標;
  2. 在partially-young mode下:該模式會選擇全部的young region,而且選擇一部分的old region。old region的選擇將依據在Marking cycle phase中對存活對象的計數。G1選擇存活對象最少的Region進行回收。

13.2.四、young gc的完整流程

YoungGC並非說現有的Eden區放滿了就會立刻觸發,G1會計算下如今Eden區回收大概要多久時間,若是回收時間遠遠小於參數 -XX:MaxGCPauseMills 設定的值,那麼增長年輕代的region,繼續給新對象存放,不會立刻作Young GC,直到下一次Eden區放滿,G1計算回收時間接近參數 -XX:MaxGCPauseMills 設定的值,那麼就會觸發Young GC

13.2.五、Mixed GC的完整流程

不是FullGC,老年代的堆佔有率達到參數(-XX:InitiatingHeapOccupancyPercent)設定的值則觸發,回收全部的Young和部分Old(根據指望的GC停頓時間肯定old區垃圾收集的優先順序)以及大對象區,正常狀況G1的垃圾收集是先作MixedGC,主要使用複製算法,須要把各個region中存活的對象拷貝到別的region裏去,拷貝過程當中若是發現沒有足夠的空region可以承載拷貝對象就會觸發一次Full GC

13.2.六、Full GC

中止系統程序,而後採用單線程進行標記、清理和壓縮整理,好空閒出來一批Region來供下一次MixedGC使用,這個過程是很是耗時的。(Shenandoah優化成多線程收集了)

13.2.七、Marking bitmaps/TAMS

Marking bitmap是一種數據結構,其中的每個bit表明的是一個可用於分配給對象的起始地址。舉例來講:

img

bitmap

其中addrN表明的是一個對象的起始地址。綠色的塊表明的是在該起始地址處的對象是存活對象,而其他白色的塊則表明了垃圾對象。
G1使用了兩個bitmap,一個叫作previous bitmap,另一個叫作next bitmap。previous bitmap記錄的是上一次的標記階段完成以後的構造的bitmap;next bitmap則是當前正在標記階段正在構造的bitmap。在當前標記階段結束以後,當前標記的next bitmap就變成了下一次標記階段的previous bitmap。

TAMS(top at mark start)變量,是一對用於區分在標記階段新分配對象的變量,分別被稱爲previous TAMS和next TAMS。在previous TAMS和next TAMS之間的對象則是本次標記階段時候新分配的對象。如圖:

img

previous TMAS 和 next TAMS

白色region表明的是空閒空間,綠色region表明是存活對象,橙色region表明的在這次標記階段新分配的對象。注意的是,在橙色區域的對象,並不能確保它們都事實上是存活的。

13.2.八、Pause Prediction Model

停頓預測模型,經過用戶設定的GC停頓時間(參數-XX:MaxGCPauseMillis),G1以衰減平均值爲理論基礎,計算須要回收的Region數量從而進行知足。

13.2.九、G1收集器參數設置

-XX:+UseG1GC:使用G1收集器

    -XX:ParallelGCThreads:指定GC工做的線程數量

    -XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分爲2048個分區

    -XX:MaxGCPauseMillis:目標暫停時間(默認200ms)

    -XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%)

    -XX:G1MaxNewSizePercent:新生代內存最大空間

    -XX:TargetSurvivorRatio:Survivor區的填充容量(默認50%),Survivor區域裏的一批對象(年齡1+年齡2+年齡n的多個年齡對象)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代

    -XX:MaxTenuringThreshold:最大年齡閾值(默認15)

    -XX:InitiatingHeapOccupancyPercent:老年代佔用空間達到整堆內存閾值(默認45%),則執行新生代和老年代的混合收集(MixedGC),好比咱們以前說的堆默認有2048個region,若是有接近1000個region都是老年代的region,則可能就要觸發MixedGC了

    -XX:G1MixedGCLiveThresholdPercent(默認85%)  region中的存活對象低於這個值時纔會回收該region,若是超過這個值,存活對象過多,回收的的意義不大。

    -XX:G1MixedGCCountTarget:在一次回收過程當中指定作幾回篩選回收(默認8次),在最後一個篩選回收階段能夠回收一會,而後暫停回收,恢復系統運行,一會再開始回收,這樣可讓系統不至於單次停頓時間過長。

    -XX:G1HeapWastePercent(默認5%): gc過程當中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基於複製算法進行的,都是把要回收的Region裏的存活對象放入其餘Region,而後這個Region中的垃圾對象所有清理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閒出來的Region數量達到了堆內存的5%,此時就會當即中止混合回收,意味着本次混合回收就結束了。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

13.2.十、G1垃圾收集器優化建議( -XX:MaxGCPauseMills=50ms)

​ 假設參數 -XX:MaxGCPauseMills 設置的值很大,致使系統運行好久,年輕代可能都佔用了堆內存的60%了,此時才觸發年輕代gc。
​ 那麼存活下來的對象可能就會不少,此時就會致使Survivor區域放不下那麼多的對象,就會進入老年代中。

​ 或者是你年輕代gc事後,存活下來的對象過多,致使進入Survivor區域後觸發了動態年齡斷定規則,達到了Survivor區域的50%,也會快速致使一些對象進入老年代中。
​ 因此這裏核心仍是在於調節 -XX:MaxGCPauseMills 這個參數的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc事後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc.

13.2.十一、什麼場景適合使用G1

  1. 50%以上的堆被存活對象佔用
  2. 對象分配和晉升的速度變化很是大
  3. 垃圾回收時間特別長,超過1秒
  4. 8GB以上的堆內存(建議值)
  5. 停頓時間是500ms之內

13.三、ZGC

定義:具備實驗性質的低延遲垃圾收集器

13.3.一、主要目標

  • 支持TB量級的堆。咱們生產環境的硬盤尚未上TB呢,這應該能夠知足將來十年內,全部JAVA應用的需求了吧。
  • 最大GC停頓時間不超10ms。目前通常線上環境運行良好的JAVA應用Minor GC停頓時間在10ms左右,Major GC通常都須要100ms以上(G1能夠調節停頓時間,可是若是調的太低的話,反而會拔苗助長),之因此能作到這一點是由於它的停頓時間主要跟Root掃描有關,而Root數量和堆大小是沒有任何關係的。
  • 奠基將來GC特性的基礎。
  • 最糟糕的狀況下吞吐量會下降15%。這都不是事,停頓時間足夠優秀。至於吞吐量,經過擴容分分鐘解決。另外,Oracle官方提到了它最大的優勢是:它的停頓時間不會隨着堆的增大而增加!也就是說,幾十G堆的停頓時間是10ms如下,幾百G甚至上T堆的停頓時間也是10ms如下。

13.3.二、color poin(顏色指針)

Colored Pointers,即顏色指針,以下圖所示,ZGC的核心設計之一。之前的垃圾回收器的GC信息都保存在對象頭中,而ZGC的GC信息保存在指針中。

在這裏插入圖片描述每一個對象有一個64位指針,這64位被分爲:

  • 18位:預留給之後使用;
  • 1位:Finalizable標識,此位與併發引用處理有關,它表示這個對象只能經過finalizer才能訪問;
  • 1位:Remapped標識,設置此位的值後,對象未指向relocation set中(relocation set表示須要GC的
    Region集合);
  • 1位:Marked1標識;
  • 1位:Marked0標識,和上面的Marked1都是標記對象用於輔助GC;
  • 42位:對象的地址(因此它能夠支持2^42=4T內存):

爲何有2個mark標記?

每個GC週期開始時,會交換使用的標記位,使上次GC週期中修正的已標記狀態失效,全部引用都變成未標記。
GC週期1:使用mark0, 則週期結束全部引用mark標記都會成爲01。
GC週期2:使用mark1, 則期待的mark標記10,全部引用都能被從新標記。
經過對配置ZGC後對象指針分析咱們可知,對象指針必須是64位,那麼ZGC就沒法支持32位操做系統,一樣的也就沒法支持壓縮指針了(CompressedOops,壓縮指針也是32位)。

顏色指針的三大優點:

  1. 一旦某個Region的存活對象被移走以後,這個Region當即就可以被釋放和重用掉,而沒必要等待整個堆中全部指向該Region的引用都被修正後才能清理,這使得理論上只要還有一個空閒Region,ZGC就能完成收集。
  2. 顏色指針能夠大幅減小在垃圾收集過程當中內存屏障的使用數量,ZGC只使用了讀屏障。
  3. 顏色指針具有強大的擴展性,它能夠做爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便往後進一步提升性能。

讀屏障
以前的GC都是採用Write Barrier,此次ZGC採用了徹底不一樣的方案讀屏障,這個是ZGC一個很是重要的特性。
在標記和移動對象的階段,每次「從堆裏對象的引用類型中讀取一個指針」的時候,都須要加上一個Load Barriers。那麼咱們該如何理解它呢?看下面的代碼,第一行代碼咱們嘗試讀取堆中的一個對象引用obj.fieldA並賦給引用o(fieldA也是一個對象時纔會加上讀屏障)。若是這時候對象在GC時被移動了,接下來JVM就會加上一個讀屏障,這個屏障會把讀出的指針更新到對象的新地址上,而且把堆裏的這個指針「修正」到本來的字段裏。這樣就算GC把對象移動了,讀屏障也會發現並修正指針,因而應用代碼就永遠都會持有更新後的有效指針,並且不須要STW。
那麼,JVM是如何判斷對象被移動過呢?就是利用上面提到的顏色指針,若是指針是Bad Color,那麼程序還不能往下執行,須要「slow path」,修正指針;若是指針是Good Color,那麼正常往下執行便可:

在這裏插入圖片描述

❝ 這個動做是否是很是像JDK併發中用到的CAS自旋?讀取的值發現已經失效了,須要從新讀取。而ZGC這裏是以前持有的指針因爲GC後失效了,須要經過讀屏障修正指針。❞
後面3行代碼都不須要加讀屏障:Object p = o這行代碼並無從堆中讀取數據:o.doSomething()也沒有從堆中讀取數據;obj.fieldB不是對象引用,而是原子類型。
正是由於Load Barriers的存在,因此會致使配置ZGC的應用的吞吐量會變低。官方的測試數據是須要多出額外4%的開銷:

在這裏插入圖片描述

那麼,判斷對象是Bad Color仍是Good Color的依據是什麼呢?就是根據上一段提到的Colored Pointers的4個顏色位。

當加上讀屏障時,根據對象指針中這4位的信息,就能知道當前對象是Bad/Good Color了。

PS:既然低42位指針能夠支持4T內存,那麼可否經過預定更多位給對象地址來達到支持更大內存的目的呢?答案確定是不能夠。由於目前主板地址總線最寬只有48bit,4位是顏色位,就只剩44位了,因此受限於目前的硬件,ZGC最大隻能支持16T的內存,JDK13就把最大支持堆內存從4T擴大到了16T。

13.3.三、運做過程

在這裏插入圖片描述

  1. 併發標記(Concurrent Mark):與G1同樣,併發標記是遍歷對象圖作可達性分析的階段,它的初始標記(Mark Start)和最終標記(Mark End)也會出現短暫的停頓,與G1不一樣的是, ZGC的標記是在指針上而不是在對象上進行的, 標記階段會更新染色指針中的Marked 0、 Marked 1標誌位。
  2. 併發預備重分配(Concurrent Prepare for Relocate):這個階段須要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會掃描全部的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。
  3. 併發重分配(Concurrent Relocate):重分配是ZGC執行過程當中的核心階段,這個過程要把重分配集中的存活對象複製到新的Region上,併爲重分配集中的每一個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關係。ZGC收集器能僅從引用上就明確得知一個對象是否處於重分配集之中,若是用戶線程此時併發訪問了位於重分配集中的對象,此次訪問將會被預置的內存屏障(讀屏障)所截獲,而後當即根據Region上的轉發表記錄將訪問轉發到新複製的對象上,並同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行爲稱爲指針的「自愈」(Self-Healing)能力。
    1 ZGC的顏色指針由於「自愈」(Self‐Healing)能力,因此只有第一次訪問舊對象會變慢, 一旦重分配集中某個Region的存活對象都複製完畢後,
    2 這個Region就能夠當即釋放用於新對象的分配,可是轉發表還得留着不能釋放掉, 由於可能還有訪問在使用這個轉發表。
  4. 併發重映射(Concurrent Remap):重映射所作的就是修正整個堆中指向重分配集中舊對象的全部引用,可是ZGC中對象引用存在「自愈」功能,因此這個重映射操做並非很迫切。ZGC很巧妙地把併發重映射階段要作的工做,合併到了下一次垃圾收集循環中的併發標記階段裏去完成,反正它們都是要遍歷全部對象的,這樣合併就節省了一次遍歷對象圖的開銷。一旦全部指針都被修正以後, 原來記錄新舊對象關係的轉發表就能夠釋放掉了。

13.3.四、存在的問題,怎麼解決

ZGC最大的問題是浮動垃圾。ZGC的停頓時間是在10ms如下,可是ZGC的執行時間仍是遠遠大於這個時間的。假如ZGC全過程須要執行10分鐘,在這個期間因爲對象分配速率很高,將建立大量的新對象,這些對象很難進入當次GC,因此只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
ZGC沒有分代概念,每次都須要進行全堆掃描,致使一些「朝生夕死」的對象沒能及時的被回收。

解決方案
目前惟一的辦法是增大堆的容量,使得程序獲得更多的喘息時間,可是這個也是一個治標不治本的方案。若是須要從根本上解決這個問題,仍是須要引入分代收集,讓新生對象都在一個專門的區域中建立,而後專門針對這個區域進行更頻繁、更快的收集。

13.3.五、安全點與安全區域

安全點就是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態是肯定的,這樣JVM就能夠安全的進行一些操做,好比GC等,因此GC不是想何時作就當即觸發的,是須要等待全部線程運行到安全點後才能觸發。
這些特定的安全點位置主要有如下幾種:

  1. 方法返回以前
  2. 調用某個方法以後
  3. 拋出異常的位置
  4. 循環的末尾

大致實現思想是當垃圾收集須要中斷線程的時候, 不直接對線程操做, 僅僅簡單地設置一個標誌位, 各個線程執行過程時會不停地主動去輪詢這個標誌, 一旦發現中斷標誌爲真時就本身在最近的安全點上主動中斷掛起。 輪詢標誌的地方和安全點是重合的。

安全區域又是什麼?

Safe Point 是對正在執行的線程設定的。
若是一個線程處於 Sleep 或中斷狀態,它就不能響應 JVM 的中斷請求,再運行到 Safe Point 上。所以 JVM 引入了 Safe Region。
Safe Region 是指在一段代碼片斷中,引用關係不會發生變化。在這個區域內的任意地方開始 GC 都是安全的。

13.3.六、ZGC參數

在這裏插入圖片描述

13.3.七、ZGC觸發時機

ZGC目前有4中機制觸發GC:

一、定時觸發,默認爲不使用,可經過ZCollectionInterval參數配置。
二、預熱觸發,最多三次,在堆內存達到10%、20%、30%時觸發,主要時統計GC時間,爲其餘GC機制使用。
三、分配速率,基於正態分佈統計,計算內存99.9%可能的最大分配速率,以及此速率下內存將要耗盡的時間點,在耗盡以前觸發GC(耗盡時間 - 一次GC最大持續時間 - 一次GC檢測週期時間)。
四、主動觸發,(默認開啓,可經過ZProactive參數配置) 距上次GC堆內存增加10%,或超過5分鐘時,對比距上次GC的間隔時間跟(49 * 一次GC的最大持續時間),超過則觸發。

1四、如何選擇垃圾收集器

  1. 優先調整堆的大小讓服務器本身來選擇

  2. 若是內存小於100M,使用串行收集器

  3. 若是是單核,而且沒有停頓時間的要求,串行或JVM本身選擇

  4. 若是容許停頓時間超過1秒,選擇並行或者JVM本身選

  5. 若是響應時間最重要,而且不能超過1秒,使用併發收集器

  6. 4G如下能夠用parallel,4-8G能夠用ParNew+CMS,8G以上能夠用G1,幾百G以上用ZGC

在這裏插入圖片描述

JDK 1.8默認使用 Parallel(年輕代和老年代都是)
JDK 1.9默認使用 G1

1五、各類命令(例如100%cpu的排查、死鎖的檢查)

15.一、100%CPU的排查

1 、 使用top命令查看cpu佔用資源較高的PID

二、經過jps 找到當前用戶下的java程序PID(jps -l 可以打印出全部的應用的PID)

三、使用 pidstat -p

四、找到cpu佔用較高的線程TID

五、將TID轉換爲十六進制的表示方式

六、經過jstack -l(使用jstack 輸出當前PID的線程dunp信息)

七、 查找 TID對應的線程(輸出的線程id爲十六進制),找到對應的代碼

15.二、死鎖的檢查

方法1、使用 jps + jstack

​ 在windons命令窗口,使用 jps -l (找到運行的程序的PID)

​ 使用jstack -l PID(上面的)

方法二:使用jconsole

方法三:使用Java Visual VM

1六、JIT(即時編譯器)

JIT是一種提升程序運行效率的方法。一般,程序有兩種運行方式:靜態編譯與動態解釋。靜態編譯的程序在執行前所有被翻譯爲機器碼,而動態解釋執行的則是一句一句邊運行邊翻譯。

1七、逃逸分析

逃逸分析是指在某個方法以內建立的對象,除了在方法體以內被引用以外,還在方法體以外被其它變量引用到;這樣帶來的後果是在該方法執行完畢以後,該方法中建立的對象將沒法被GC回收,因爲其被其它變量引用。正常的方法調用中,方法體中建立的對象將在執行完畢以後,將回收其中建立的對象;故因爲沒法回收,即成爲逃逸。

相關文章
相關標籤/搜索