一文搞定 JVM 面试,教你吊打面试官~
1.什么是類加載?類加載的過程?
類的加載指的是將類的class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè)此類的對(duì)象,通過這個(gè)對(duì)象可以訪問到方法區(qū)對(duì)應(yīng)的類信息。
加載
通過類的全限定名獲取定義此類的二進(jìn)制字節(jié)流
將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
使用到類時(shí)才會(huì)加載,例如調(diào)用類的main( )方法,new對(duì)象等等,在加載階段會(huì)在內(nèi)存中生成一個(gè)代表該類的Class對(duì)象,作為方法區(qū)類信息的訪問入口
驗(yàn)證
準(zhǔn)備
解析
注意:這里的符號(hào)引用是指那些在編譯期間就能夠確定下來的數(shù)據(jù),包括靜態(tài)屬性、常量、私有屬性等等,因?yàn)檫@些數(shù)據(jù)不會(huì)被繼承或者被重寫,所以它們適合在類加載階段進(jìn)行解析,這就是所謂的靜態(tài)鏈接過程(類加載期間完成)。
而那些在編譯期間無法確定下來的數(shù)據(jù),就只能等到運(yùn)行期間再將符號(hào)引用替換為直接引用,這也就是動(dòng)態(tài)鏈接的過程。
初始化
2.什么是類加載器,類加載器有哪些?
實(shí)現(xiàn)通過類的全限定名獲取該類的二進(jìn)制字節(jié)流的代碼塊叫做類加載器。
Java中有如下四種類加載器
rt.jar、charsets.jar等,該加載器無法被Java程序直接引用。
類包。
些類。
3.什么是雙親委派機(jī)制?
如果一個(gè)類加載器收到了類加載請求,它并不會(huì)自己先去加載,而是把這個(gè)請求委托給父類的加載器去執(zhí)行。
如果父類加載器可以完成類加載任務(wù),就成功返回;倘若父類加載器無法完成此加載任務(wù),子類加載器才會(huì)嘗試自己去加載,這就是雙親委派機(jī)制。
父類加載器一層一層往下分配任務(wù),如果子類加載器能加載,則加載此類;如果將加載任務(wù)分配到系統(tǒng)類加載器也無法加載此類,則拋出異常。
為什么要設(shè)計(jì)雙親委派機(jī)制?
沙箱安全機(jī)制
避免類的重復(fù)加載
全盤負(fù)責(zé)委托機(jī)制
“全盤負(fù)責(zé)”是指當(dāng)一個(gè)ClassLoder裝載一個(gè)類時(shí),除非顯示的使用另外一個(gè)ClassLoder,否則該類
所依賴及引用的類也由這個(gè)ClassLoder載入。
4.如何自定義類加載器?
我們來看下應(yīng)用程序類加載器AppClassLoader加載類的雙親委派機(jī)制源碼,AppClassLoader的loadClass方法最終會(huì)調(diào)用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:
自定義類加載器
自定義類加載器只需要繼承 java.lang.ClassLoader 類,該類有兩個(gè)核心方法,一個(gè)是loadClass(String, boolean),實(shí)現(xiàn)了雙親委派機(jī)制,還有一個(gè)方法是findClass,默認(rèn)實(shí)現(xiàn)是空方法。
5.Tomcat 打破雙親委派機(jī)制
先思考一個(gè)問題,Tomcat是個(gè)web容器, 那么它要解決什么問題:
一個(gè)web容器可能需要部署多個(gè)應(yīng)用程序,不同的應(yīng)用程序可能會(huì)依賴同一個(gè)第三方類庫的不同版本,不能要求同一個(gè)類庫在同一個(gè)服務(wù)器具有多份,因此要保證每個(gè)應(yīng)用程序的類庫都是獨(dú)立的,保證相互隔離。
部署在同一個(gè)web容器中相同的類庫相同的版本可以共享。否則,如果服務(wù)器有10個(gè)應(yīng)用程序,那么要有10份相同的類庫加載進(jìn)虛擬機(jī)。
web容器也有自己依賴的類庫,不能與應(yīng)用程序的類庫混淆。基于安全考慮,應(yīng)該讓容器的類庫和程序的類庫隔離開來。
web容器要支持jsp的修改,我們知道,jsp文件最終也是要編譯成class文件才能在虛擬機(jī)中運(yùn)行,但程序運(yùn)行后修改jsp已經(jīng)是司空見慣的事情, web容器需要支持 jsp 修改后不用重啟。
以 Tomcat 類加載為例,Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行?
答案是不行的。為什么?
第一個(gè)問題,如果使用默認(rèn)的類加載器機(jī)制,那么是無法加載兩個(gè)相同類庫的不同版本的,默認(rèn)的類加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。
第二個(gè)問題,默認(rèn)的類加載器是能夠?qū)崿F(xiàn)的,因?yàn)樗穆氊?zé)就是保證唯一性。
第三個(gè)問題和第一個(gè)問題一樣。
我們再來看第四個(gè)問題,我們想要怎么實(shí)現(xiàn)jsp文件的熱加載,jsp 文件其實(shí)也就是class文件,那么如果修改了,但類名還是一樣,類加載器會(huì)直接取方法區(qū)中已經(jīng)存在的,修改后的jsp是不會(huì)重新加載的。
那怎么辦呢?我們可以直接卸載掉這個(gè)jsp文件的類加載器,所以你應(yīng)該想到了,每個(gè)jsp文件對(duì)應(yīng)一個(gè)唯一的類加載器,當(dāng)一個(gè)jsp文件修改了,就直接卸載掉這個(gè)jsp類加載器。重新創(chuàng)建jsp類加載器,重新加載jsp文件。
Tomcat自定義類加載器詳解
tomcat的幾個(gè)主要類加載器:
- commonLoader:Tomcat最基本的類加載器,加載路徑中的class可以被Tomcat容器本身以及各個(gè)Webapp訪問;
- catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對(duì)于Webapp不可見;
- sharedLoader:各個(gè)Webapp共享的類加載器,加載路徑中的class對(duì)于所有Webapp可見,但是對(duì)于Tomcat容器不可見;
- WebappClassLoader:各個(gè)Webapp私有的類加載器,加載路徑中的class只對(duì)當(dāng)前Webapp可見,比如加載war包里相關(guān)的類,每個(gè)war包應(yīng)用都有自己的WebappClassLoader,實(shí)現(xiàn)相互隔離,比如不同war包應(yīng)用引入了不同的spring版本,這樣實(shí)現(xiàn)就能加載各自的spring版本。
從圖中的委派關(guān)系中可以看出:
CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,從而實(shí)現(xiàn)了公有類庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對(duì)方相互隔離。
WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個(gè)WebAppClassLoader實(shí)例之間相互隔離。
而JasperLoader的加載范圍僅僅是這個(gè)JSP文件所編譯出來的那一個(gè).Class文件,它出現(xiàn)的目的就是為了被丟棄:當(dāng)Web容器檢測到JSP文件被修改時(shí),會(huì)替換掉當(dāng)前的JasperLoader實(shí)例,并通過再創(chuàng)建一個(gè)新的JSP類加載器來實(shí)現(xiàn)JSP文件的熱加載功能。
tomcat 這種類加載機(jī)制違背了Java 推薦的雙親委派模型了嗎?
答案是:違背了。
很顯然,tomcat 不是這樣實(shí)現(xiàn)的,tomcat 為了實(shí)現(xiàn)隔離性,沒有遵守這個(gè)約定,每個(gè)webappClassLoader 加載自己的目錄下的class文件,不會(huì)傳遞給父加載器,打破了雙親委派機(jī)制。
Tomcat的JasperLoader熱加載
后臺(tái)啟動(dòng)線程監(jiān)聽jsp文件變化,如果變化了找到該jsp對(duì)應(yīng)的servlet類的加載器引用(GCRoots),重新生成新的JasperLoader加載器并賦值給該引用;
然后加載新的jsp對(duì)應(yīng)的servlet類,之前的那個(gè)加載器因?yàn)闆]有GCRoots引用了,下一次GC的時(shí)候會(huì)被銷毀。
6.JVM內(nèi)存模型(運(yùn)行時(shí)數(shù)據(jù)區(qū))
類加載后的大的Class對(duì)象是存放在堆中,math對(duì)象應(yīng)該是指向方法區(qū)中的類元信息,而類元信息再去指向堆中的大的Class對(duì)象
public class Math {public static final int initData = 666;public static User user = new User();public int compute() {int a = 1;int b = 2;int c = (a + b) * 10;return c;}public static void main(String[] args) {Math math = new Math();math.compute();System.out.println("test");}}7.JVM內(nèi)存參數(shù)設(shè)置
棧和方法區(qū)所占用的內(nèi)存空間都是堆外的本地內(nèi)存
Spring Boot程序的JVM參數(shù)設(shè)置格式
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar-Xss:每個(gè)線程的棧大小
-Xms:設(shè)置堆的初始可用大小,默認(rèn)物理內(nèi)存的1/64
-Xmx:設(shè)置堆的最大可用大小,默認(rèn)物理內(nèi)存的1/4
-Xmn:新生代大小
-XX:NewRatio:默認(rèn)2表示新生代占老年代的1/2,占整個(gè)堆內(nèi)存的1/3。
-XX:SurvivorRatio:默認(rèn)8表示一個(gè)survivor區(qū)占用1/8的Eden內(nèi)存,即1/10的新生代內(nèi)存。
關(guān)于元空間的JVM參數(shù)有兩個(gè):-XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 設(shè)置元空間最大值, 默認(rèn)是-1, 即不受限制, 或者說只受限于本地內(nèi)存大小。
-XX:MetaspaceSize: 指定元空間觸發(fā)Full GC的初始閾值(元空間無固定初始大小), 以字節(jié)為單位,默認(rèn)是21M左右,達(dá)到該值就會(huì)觸發(fā)Full GC進(jìn)行類型卸載, 同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間, 就適當(dāng)降低該值; 如果釋放了很少的空間, 那么在不超過 -XX:MaxMetaspaceSize(如果設(shè)置了的話) 的情況下, 適當(dāng)提高該值。
這個(gè)跟早期JDK版本的**-XX:PermSize**參數(shù)意思不一樣,-XX:PermSize代表永久代的初始容量。
由于調(diào)整元空間的大小需要Full GC,這是非常昂貴的操作,如果應(yīng)用在啟動(dòng)的時(shí)候發(fā)生大量Full GC,通常都是由于永久代或元空間發(fā)生了大小調(diào)整,基于這種情況,一般建議在JVM參數(shù)中將MetaspaceSize和MaxMetaspaceSize設(shè)置成一樣的值,并設(shè)置得比初始值要大,對(duì)于8G物理內(nèi)存的機(jī)器來說,一般我會(huì)將這兩個(gè)值都設(shè)置為256M。
StackOverflowError示例:
// JVM設(shè)置 -Xss128k(默認(rèn)1M) public class StackOverflowTest {static int count = 0;static void redo() {count++;redo();}public static void main(String[] args) {try {redo();} catch (Throwable t) {t.printStackTrace();System.out.println(count);}} }運(yùn)行結(jié)果: java.lang.StackOverflowErrorat com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)......注意:
-Xss設(shè)置越小count值越小,說明一個(gè)線程棧里能分配的棧幀就越少,但是對(duì)JVM整體來說能開啟的線程數(shù)會(huì)更多。
8.對(duì)象的創(chuàng)建
對(duì)象創(chuàng)建的主要流程:
類加載檢查
虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
new指令對(duì)應(yīng)到語言層面上講是,new關(guān)鍵詞、對(duì)象克隆、反射、對(duì)象序列化等。
分配內(nèi)存
在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。
這個(gè)步驟有兩個(gè)問題:
如何劃分內(nèi)存。
在并發(fā)情況下, 可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒來得及修改,對(duì)象B又同時(shí)使用了原來的指針來分配內(nèi)存的情況。
劃分內(nèi)存的方法:
- “指針碰撞”(Bump the Pointer,默認(rèn)用指針碰撞)
如果Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離。
- “空閑列表”(Free List,CMS收集器使用這種方式)
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例, 并更新列表上的記錄。
解決并發(fā)問題的方法:
- CAS(compare and swap)
虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性來對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理。
- 本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)
把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存。通過 -XX:+/-UseTLAB 參數(shù)來設(shè)定虛擬機(jī)是否使用 TLAB(JVM會(huì)默認(rèn)開啟 -XX:+UseTLAB),-XX:TLABSize:指定TLAB大小。
初始化零值
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭), 如果使用TLAB,這一工作過程也可以提前至TLAB分配時(shí)進(jìn)行。
這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
設(shè)置對(duì)象頭
初始化零值之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭Object Header之中。
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、 實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
32位對(duì)象頭
64位對(duì)象頭
關(guān)于對(duì)其填充
對(duì)于大部分處理器,對(duì)象以8字節(jié)整數(shù)倍來對(duì)齊填充都是最高效的存取方式。
什么是指針壓縮?
為什么要進(jìn)行指針壓縮?
執(zhí)行 init 方法
執(zhí)行init方法,即對(duì)象按照程序員的意愿進(jìn)行初始化。對(duì)應(yīng)到語言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值)和執(zhí)行構(gòu)造方法。
9.對(duì)象內(nèi)存分配
對(duì)象內(nèi)存分配流程圖
對(duì)象棧上分配
我們通過JVM內(nèi)存分配可以知道JAVA中的對(duì)象都是在堆上進(jìn)行分配,當(dāng)對(duì)象沒有被引用的時(shí)候,需要依靠GC進(jìn)行內(nèi)存回收,如果對(duì)象數(shù)量較多的時(shí)候,會(huì)給GC帶來較大壓力,也間接影響了應(yīng)用的性能。
為了減少臨時(shí)對(duì)象在堆內(nèi)分配的數(shù)量,JVM通過逃逸分析確定該對(duì)象不會(huì)被外部訪問。如果不會(huì)逃逸則可以將該對(duì)象在棧上分配內(nèi)存,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
對(duì)象逃逸分析
就是分析對(duì)象動(dòng)態(tài)作用域,當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中。
public User test1() {User user = new User();user.setId(1);user.setName("zhuge");//TODO 保存到數(shù)據(jù)庫return user; }public void test2() {User user = new User();user.setId(1);user.setName("zhuge");//TODO 保存到數(shù)據(jù)庫 }很顯然test1方法中的user對(duì)象被返回了,這個(gè)對(duì)象的作用域范圍不確定,test2方法中的user對(duì)象我們可以確定當(dāng)方法結(jié)束時(shí)這個(gè)對(duì)象就是無效對(duì)象了,對(duì)于這樣的對(duì)象我們其實(shí)可以將其分配在棧內(nèi)存里,讓其在方法結(jié)束時(shí)跟隨棧內(nèi)存一起被回收掉。
JVM對(duì)于這種情況可以通過開啟逃逸分析參數(shù)(-XX:+DoEscapeAnalysis)來優(yōu)化對(duì)象內(nèi)存分配位置,使其通過標(biāo)量替換優(yōu)先分配在棧上(棧上分配),JDK7之后默認(rèn)開啟逃逸分析,如果要關(guān)閉使用參數(shù)(-XX:-DoEscapeAnalysis)
標(biāo)量替換
通過逃逸分析確定該對(duì)象不會(huì)被外部訪問,并且對(duì)象可以被進(jìn)一步分解時(shí),JVM不會(huì)創(chuàng)建該對(duì)象,而是將該對(duì)象的成員變量分解成若干個(gè)被這個(gè)方法使用的成員變量所代替,這些代替的成員變量就在棧幀或寄存器上分配空間。開啟標(biāo)量替換參數(shù)(-XX:+EliminateAllocations),JDK7之后默認(rèn)開啟。
標(biāo)量與聚合量
標(biāo)量即不可被進(jìn)一步分解的量,而JAVA的基本數(shù)據(jù)類型就是標(biāo)量(如:int、long等基本數(shù)據(jù)類型以及reference類型等),標(biāo)量的對(duì)立就是可以被進(jìn)一步分解的量,而這種量稱之為聚合量。而在JAVA中對(duì)象就是可以被進(jìn)一步分解的聚合量。
棧上分配示例:
/*** 棧上分配,標(biāo)量替換* 代碼調(diào)用了1億次alloc(),如果是分配到堆上,大概需要1GB以上的堆空間,如果堆空間小于該值,必然會(huì)觸發(fā)GC。* * 使用如下參數(shù)不會(huì)發(fā)生GC* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations* 使用如下參數(shù)都會(huì)發(fā)生大量GC* -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations* +DoEscapeAnalysis:開啟逃逸分析,-XX:+EliminateAllocations:開啟標(biāo)量替換*/ public class AllotOnStack {public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 100000000; i++) {alloc();}long end = System.currentTimeMillis();System.out.println(end - start);}private static void alloc() {User user = new User();user.setId(1);user.setName("zhuge");} }結(jié)論:棧上分配依賴于逃逸分析和標(biāo)量替換
10.對(duì)象在Eden區(qū)分配
大多數(shù)情況下,對(duì)象在新生代中的 Eden 區(qū)分配。當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次 Minor GC。
Minor GC 和 Full GC
- Minor GC/Young GC:指發(fā)生新生代的的垃圾收集動(dòng)作,Minor GC非常頻繁,回收速度一般也比較快。
- Major GC/Full GC:一般會(huì)回收老年代 、年輕代、方法區(qū)的垃圾,Major GC的速度一般會(huì)比Minor GC 慢10倍以上。
Eden : s0 : s1 = 8 : 1 : 1
大對(duì)象直接進(jìn)入老年代
大對(duì)象就是需要大量連續(xù)內(nèi)存空間的對(duì)象(比如:字符串、數(shù)組)。
當(dāng)Eden區(qū)的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建新對(duì)象,JVM的垃圾收集器將對(duì)Eden區(qū)進(jìn)行垃圾回收(Minor GC / Young GC),將Eden區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀。再加載新的對(duì)象放到Eden區(qū)。如果觸發(fā)MinorGC后對(duì)象還是無法放在Eden區(qū),說明是超大對(duì)象,則直接將對(duì)象放到老年代。
JVM參數(shù) -XX:PretenureSizeThreshold 可以設(shè)置大對(duì)象的大小,如果對(duì)象超過設(shè)置大小會(huì)直接進(jìn)入老年代,不會(huì)進(jìn)入年輕代,這個(gè)參數(shù)只在 Serial 和 ParNew 兩個(gè)收集器下有效。
比如設(shè)置JVM參數(shù):-XX:PretenureSizeThreshold=1000000(單位是字節(jié),只有Serial和ParNew的收集器有效)
為什么要這樣設(shè)計(jì)呢?
為了避免為大對(duì)象分配內(nèi)存時(shí)的復(fù)制操作而降低效率。
長期存活的對(duì)象將進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代中。為了做到這一點(diǎn),虛擬機(jī)給每個(gè)對(duì)象設(shè)置一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。
如果對(duì)象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動(dòng)到 Survivor 空間中,并將對(duì)象年齡設(shè)為1。
對(duì)象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲,CMS收集器默認(rèn)6歲,不同的垃圾收集器會(huì)略微有點(diǎn)不同),就會(huì)被晉升到老年代中。
對(duì)象晉升到老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置。
對(duì)象動(dòng)態(tài)年齡判斷
當(dāng)前存放對(duì)象的Survivor區(qū)域里(其中一塊區(qū)域,放對(duì)象的那塊s區(qū)),一批對(duì)象的總大小大于這塊Survivor區(qū)域內(nèi)存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此時(shí)大于等于這批對(duì)象年齡最大值的對(duì)象,就可以直接進(jìn)入老年代了;
例如Survivor區(qū)域里現(xiàn)在有一批對(duì)象,年齡1+年齡2+年齡n 的多個(gè)年齡對(duì)象總和超過了Survivor區(qū)域的50%,此時(shí)就會(huì)把年齡n(含)以上的對(duì)象都放入老年代。
這個(gè)規(guī)則其實(shí)是希望那些可能是長期存活的對(duì)象,盡早進(jìn)入老年代。對(duì)象動(dòng)態(tài)年齡判斷機(jī)制一般是在 Minor GC 之后觸發(fā)的。
老年代空間分配擔(dān)保機(jī)制
年輕代每次minor gc之前JVM都會(huì)計(jì)算下老年代剩余可用空間
如果這個(gè)可用空間小于年輕代里現(xiàn)有的所有對(duì)象大小之和(包括垃圾對(duì)象)
就會(huì)看一個(gè) “-XX:-HandlePromotionFailure”(jdk1.8默認(rèn)就設(shè)置了)的參數(shù)是否設(shè)置了
如果有這個(gè)參數(shù),就會(huì)看看老年代的可用內(nèi)存大小,是否大于之前每一次 minor gc 后進(jìn)入老年代的對(duì)象的平均大小。
如果上一步結(jié)果是小于或者前面說的參數(shù)沒有設(shè)置,那么就會(huì)觸發(fā)一次 Full gc,對(duì)老年代和年輕代一起回收一次垃圾,如果回收完還是沒有足夠空間存放新的對(duì)象就會(huì)發(fā)生 “OOM”
當(dāng)然,如果minor gc之后剩余存活的需要挪動(dòng)到老年代的對(duì)象大小還是大于老年代可用空間,那么也會(huì)觸發(fā)Full gc,Full gc 完之后如果還是沒有空間放minor gc之后的存活對(duì)象,則也會(huì)發(fā)生“OOM”。
11.對(duì)象內(nèi)存回收
堆中幾乎存放著所有的對(duì)象實(shí)例,對(duì)堆進(jìn)行垃圾回收前的第一步就是要判斷哪些對(duì)象已經(jīng)死亡(即不能再被任何途徑使用的對(duì)象)。
引用計(jì)數(shù)法
給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它,計(jì)數(shù)器就加1;
當(dāng)引用失效時(shí),計(jì)數(shù)器就會(huì)減1,任何時(shí)候計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的對(duì)象。
這個(gè)方法實(shí)現(xiàn)簡單,效率高,但是目前主流的虛擬機(jī)中并沒有選擇這個(gè)算法來管理內(nèi)存,其最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問題。
所謂對(duì)象之間的相互循環(huán)引用問題,如下面代碼所示:除了對(duì)象objA 和 objB 相互引用著對(duì)方之外,這兩個(gè)對(duì)象之間再無任何引用。但是因?yàn)樗麄兓ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)器都不為0,于是引用計(jì)數(shù)算法無法通知 GC 回收器回收他們。
public class ReferenceCountingGc {Object instance = null;public static void main(String[] args) {ReferenceCountingGc objA = new ReferenceCountingGc();ReferenceCountingGc objB = new ReferenceCountingGc();objA.instance = objB;objB.instance = objA;objA = null;objB = null;} }可達(dá)性分析算法
將 “GC Roots” 對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索引用到的對(duì)象,找到的對(duì)象都標(biāo)記為非垃圾對(duì)象,其余未標(biāo)記的對(duì)象都是垃圾對(duì)象。
**GC Roots **根節(jié)點(diǎn)有哪些?
finalize( ) 方法最終判定對(duì)象是否存活
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷再次標(biāo)記過程。
標(biāo)記的前提是對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈。
第一次標(biāo)記并進(jìn)行一次篩選
篩選的條件是此對(duì)象是否有必要執(zhí)行finalize( )方法。當(dāng)對(duì)象沒有重寫finalize( )方法時(shí),對(duì)象將直接被回收。
第二次標(biāo)記
如果這個(gè)對(duì)象覆蓋了finalize方法,finalize方法是對(duì)象脫逃死亡命運(yùn)的最后一次機(jī)會(huì),如果對(duì)象要在finalize( )中成功拯救自己,只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,
譬如把自己賦值給某個(gè)類變量或?qū)ο蟮某蓡T變量,那在第二次標(biāo)記時(shí)它將移除出“即將回收”的集合。如果對(duì)象這時(shí)候還沒逃脫,那基本上它就真的被回收了。
注意:一個(gè)對(duì)象的finalize( )方法只會(huì)被執(zhí)行一次,也就是說通過調(diào)用finalize方法自我救命的機(jī)會(huì)只有一次。
12.常見引用類型
Java 的引用類型一般分為四種:強(qiáng)引用、軟引用、弱引用、虛引用。
強(qiáng)引用
指在程序代碼中普遍存在的引用賦值,類似 “Object obj = new Object( )” 這種引用關(guān)系;如果內(nèi)存空間不足了,GC 寧愿拋出 OutOfMemoryError,也不會(huì)回收具有強(qiáng)引用的對(duì)象。
軟引用
用來描述一些還有用,但非必需的對(duì)象,例如緩存數(shù)據(jù)。當(dāng)內(nèi)存足夠時(shí),不會(huì)回收軟引用的可達(dá)對(duì)象;當(dāng)內(nèi)存不夠時(shí),才會(huì)回收軟引用的可達(dá)對(duì)象。
SoftReference<User> user = new SoftReference<User>(new User());軟引用在實(shí)際中有重要的應(yīng)用,例如瀏覽器的后退按鈕。按后退時(shí),這個(gè)后退顯示的網(wǎng)頁內(nèi)容是重新進(jìn)行請求還是從緩存中取出呢?
我們就可以用到軟引用,將后退顯示的網(wǎng)頁內(nèi)容緩存起來,后面需要點(diǎn)擊后退按鈕時(shí)就無需重新進(jìn)行請求可以快速響應(yīng)。當(dāng)內(nèi)存空間不足時(shí),就會(huì)回收掉這些軟引用的對(duì)象。
因?yàn)檫@些緩存數(shù)據(jù)是可有可無的,有的話更好,沒有的話也不影響。
弱引用
用來描述那些非必需的對(duì)象,如果一個(gè)對(duì)象只具有弱引用,不管內(nèi)存空間是否充足,都會(huì)在下一次 GC 時(shí)被回收。
WeakReference<User> user = new WeakReference<User>(new User());虛引用
如果一個(gè)對(duì)象只具有虛引用,那么它就和沒有任何引用一樣,任何時(shí)候都可能被 GC 回收。
13.如何判斷一個(gè)類是無用的類
方法區(qū)主要回收的是無用的類,那么如何判斷一個(gè)類是無用的類呢?
類需要同時(shí)滿足下面3個(gè)條件才能算是 “無用的類” :
- 該類所有的對(duì)象實(shí)例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實(shí)例。
- 加載該類的 ClassLoader 已經(jīng)被回收(自定義類加載器可以被回收,比如jsp類加載器)。
- 該類對(duì)應(yīng)的 java.lang.Class 對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
這三個(gè)條件是非常苛刻的,所以當(dāng)我們做完Full GC后,元空間是釋放不出什么空間的,因?yàn)闆]有太多的類是能被回收的。當(dāng)然也有特殊的情況,就是tomcat自定義的類加載器,jsp類加載器這些就可以被回收。
14.垃圾收集算法
分代收集理論
當(dāng)前虛擬機(jī)的垃圾收集都采用分代收集算法,根據(jù)對(duì)象存活周期的不同將內(nèi)存分為幾塊。一般將java堆分為新生代和老年代,這樣我們就可以根據(jù)各個(gè)年代的特點(diǎn)選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會(huì)有大量對(duì)象(近99%)死去,所以可以選擇復(fù)制算法,只需要付出少量對(duì)象的復(fù)制成本就可以完成每次垃圾收集。
而老年代的對(duì)象存活幾率是比較高的,而且沒有額外的空間對(duì)它進(jìn)行分配擔(dān)保,所以我們必須選擇“標(biāo)記-清除”或“標(biāo)記-整理”算法進(jìn)行垃圾收集。
注意,“標(biāo)記-清除”或“標(biāo)記-整理”算法會(huì)比復(fù)制算法慢10倍以上。
復(fù)制算法
為了解決效率問題,“復(fù)制算法”出現(xiàn)了。它可以將內(nèi)存分為大小相同的兩塊,每次使用其中的一塊。
當(dāng)這一塊的內(nèi)存使用完后,就將還存活的對(duì)象復(fù)制到另一塊去,然后再把使用的空間一次性清理掉。這樣就使得每次的內(nèi)存回收都是對(duì)內(nèi)存區(qū)間的一半進(jìn)行回收。
在Minor GC過程中對(duì)象被挪動(dòng)后,引用如何修改?
對(duì)象在堆內(nèi)部挪動(dòng)的過程其實(shí)是復(fù)制,原有區(qū)域?qū)ο筮€在,一般不直接清理,JVM內(nèi)部清理過程只是將對(duì)象分配指針移動(dòng)到原有區(qū)域的頭位置即可。
比如掃描S0區(qū)域,掃到GC Roots引用的非垃圾對(duì)象,是將這些對(duì)象復(fù)制到S1區(qū)或老年代,最后掃描完了再將S0區(qū)域的對(duì)象分配指針移動(dòng)到S0區(qū)域的起始位置即可,S0區(qū)域之前的對(duì)象并不直接清理,當(dāng)有新對(duì)象分配了,原有區(qū)域里的對(duì)象也就被覆蓋(清除)了。
Minor GC在根掃描過程中會(huì)記錄所有被掃描到的對(duì)象引用(在年輕代這些引用很少,因?yàn)榇蟛糠侄际抢鴮?duì)象不會(huì)被掃描到),如果引用的對(duì)象被復(fù)制到新地址了,最后會(huì)一并更新引用指向新地址。
標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個(gè)階段:
標(biāo)記存活的對(duì)象, 統(tǒng)一回收所有未被標(biāo)記的對(duì)象(一般選擇這種),
也可以反過來,標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象 。它是最基礎(chǔ)的收集算法,比較簡單,但是會(huì)帶來兩個(gè)明顯的問題:
標(biāo)記-整理算法
根據(jù)老年代的特點(diǎn)推出的一種標(biāo)記算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,
但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行回收,而是讓所有存活的對(duì)象向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
15.垃圾收集器
雖然我們對(duì)各個(gè)垃圾收集器進(jìn)行比較,但并非為了挑選出一個(gè)最好的收集器。因?yàn)橹钡浆F(xiàn)在為止還沒有最好的垃圾收集器出現(xiàn),更加沒有萬能的垃圾收集器,我們能做的就是根據(jù)具體的應(yīng)用場景選擇適合自己的垃圾收集器。
Serial收集器
-XX:+UseSerialGC -XX:+UseSerialOldGC
Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器了。
這個(gè)收集器是一個(gè)單線程收集器。它的 “單線程” 的意義不僅僅意味著它只會(huì)使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進(jìn)行垃圾收集工作時(shí)必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結(jié)束。
新生代采用復(fù)制算法,老年代采用標(biāo)記-整理算法。
Serial收集器有沒有優(yōu)于其他垃圾收集器的地方呢?
它簡單而高效(與其他收集器的單線程相比)。Serial收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器。它主要有兩大用途:一種用途是在JDK1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備方案。
Parallel Scavenge收集器
-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel收集器其實(shí)就是Serial收集器的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其余行為(控制參數(shù)、收集算法、回收策略等等)和Serial收集器類似。
默認(rèn)的收集線程數(shù)跟cpu核數(shù)相同,當(dāng)然也可以用參數(shù)(-XX:ParallelGCThreads)指定收集線程數(shù),但是一般不推薦修改。
Parallel Scavenge 收集器的關(guān)注點(diǎn)是吞吐量(高效率的利用CPU)。
CMS、G1 等垃圾收集器的關(guān)注點(diǎn)更多的是用戶線程的停頓時(shí)間(提高用戶體驗(yàn))。
所謂吞吐量就是CPU中用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值。 Parallel Scavenge 收集器提供了很多參數(shù)供用戶找到最合適的停頓時(shí)間或最大吞吐量,如果對(duì)于收集器運(yùn)作不太了解的話,可以選擇把內(nèi)存管理優(yōu)化交給虛擬機(jī)去完成也是一個(gè)不錯(cuò)的選擇。
新生代采用復(fù)制算法,老年代采用標(biāo)記-整理算法。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多線程和“標(biāo)記-整理”算法。
在注重吞吐量以及CPU資源的場合,都可以優(yōu)先考慮Parallel Scavenge收集器和Parallel Old收集器(JDK8默認(rèn)的新生代和老年代收集器)。
ParNew收集器
-XX:+UseParNewGC
ParNew收集器其實(shí)跟Parallel收集器很類似,區(qū)別主要在于它可以和CMS收集器配合使用。
新生代采用復(fù)制算法,老年代采用標(biāo)記-整理算法。
它是許多運(yùn)行在Server模式下的虛擬機(jī)的首要選擇。
CMS收集器(重點(diǎn))
-XX:+UseConcMarkSweepGC(old)
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。
它非常符合在注重用戶體驗(yàn)的應(yīng)用上使用,它是HotSpot虛擬機(jī)第一款真正意義上的并發(fā)收集器,它第一次實(shí)現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時(shí)工作。
運(yùn)作過程
初始標(biāo)記階段(STW):暫停所有的用戶線程(STW),并記錄下 GC Roots 能直接引用的對(duì)象,速度很快。
并發(fā)標(biāo)記階段: 并發(fā)標(biāo)記階段就是從 GC Roots 的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程, 這個(gè)過程耗時(shí)較長但是不需要暫停用戶線程(STW), 可以和用戶線程一起并發(fā)運(yùn)行。因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行,可能會(huì)導(dǎo)致已經(jīng)標(biāo)記過的對(duì)象狀態(tài)發(fā)生改變。
重新標(biāo)記階段(STW): 重新標(biāo)記階段就是為了修正并發(fā)標(biāo)記期間因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段的時(shí)間稍長,但遠(yuǎn)遠(yuǎn)比并發(fā)標(biāo)記階段的時(shí)間短。
主要用到三色標(biāo)記里的增量更新算法(見下面詳解)做重新標(biāo)記。
并發(fā)清除階段: 開啟用戶線程,同時(shí) GC 線程開始對(duì)未標(biāo)記的區(qū)域做清掃。
這個(gè)階段如果有新增對(duì)象會(huì)被直接標(biāo)記為黑色,并且不做任何處理。
并發(fā)重置階段:重置本次 GC 過程中的標(biāo)記數(shù)據(jù)。
CMS 是一款優(yōu)秀的垃圾收集器,主要優(yōu)點(diǎn):并發(fā)收集、低停頓。但是它有下面幾個(gè)明顯的缺點(diǎn):
CMS的相關(guān)核心參數(shù)
G1收集器(重點(diǎn))
G1將Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),JVM目標(biāo)是不超過2048個(gè)Region(JVM源碼里 TARGET_REGION_NUMBER 定義),實(shí)際可以超過該值,但是不推薦。
一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當(dāng)然也可以用參數(shù)"-XX:G1HeapRegionSize"手動(dòng)指定Region大小,但是推薦默認(rèn)的計(jì)算方式。
G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是可以不連續(xù)的Region集合。
默認(rèn)年輕代對(duì)堆內(nèi)存的初始占比是「5%」,如果堆大小為4096M,那么年輕代占據(jù)200MB左右的內(nèi)存,對(duì)應(yīng)大概是100個(gè)Region,可以通過“-XX:G1NewSizePercent”設(shè)置新生代初始占比。
在系統(tǒng)運(yùn)行過程中,JVM會(huì)不停地給新生代增加更多的Region,但是新生代最多占比不會(huì)超過「60%」,可以通過“-XX:G1MaxNewSizePercent”調(diào)整。年輕代中的Eden和Survivor對(duì)應(yīng)的Region也跟之前一樣,默認(rèn)「8:1:1」,假設(shè)年輕代現(xiàn)在有1000個(gè)Region,Eden區(qū)對(duì)應(yīng)800個(gè),S0對(duì)應(yīng)100個(gè),S1對(duì)應(yīng)100個(gè)。
一個(gè)Region可能之前是年輕代,之后這個(gè)Region進(jìn)行了垃圾回收,變成空的Region,那么后續(xù)這個(gè)Region就有可能會(huì)變成老年代,也就是說Region的區(qū)域功能可能會(huì)動(dòng)態(tài)變化。
G1垃圾收集器對(duì)于對(duì)象什么時(shí)候會(huì)轉(zhuǎn)移到老年代跟之前講過的原則一樣,唯一不同的是對(duì)大對(duì)象的處理。
G1有專門分配大對(duì)象的Region叫Humongous區(qū),而不是讓大對(duì)象直接進(jìn)入老年代的Region中。
在G1中,大對(duì)象的判定規(guī)則就是一個(gè)大對(duì)象超過了一個(gè)Region大小的「50%」,比如每個(gè)Region是2M,只要一個(gè)大對(duì)象超過了1M,就會(huì)被放入Humongous區(qū)中,而且一個(gè)大對(duì)象如果太大,可能會(huì)橫跨多個(gè)Region來存放。
Humongous區(qū)專門存放短期巨型對(duì)象,不用直接進(jìn)入老年代,可以節(jié)約老年代的空間,避免因?yàn)槔夏甏臻g不足而導(dǎo)致的GC開銷。
Full GC的時(shí)候除了收集年輕代和老年代之外,也會(huì)將Humongous區(qū)一并回收。
G1收集器進(jìn)行一次GC(主要是Mixed GC)的運(yùn)作過程
比如說老年代此時(shí)有1000個(gè)Region都滿了,但是根據(jù)預(yù)期停頓時(shí)間,本次垃圾回收可能只能停頓200毫秒,那么通過之前回收成本計(jì)算得知,可能回收其中800個(gè)Region剛好需要200ms,那么就只會(huì)回收800個(gè)Region(Collection Set,要回收的集合),盡量把GC導(dǎo)致的停頓時(shí)間控制在我們指定的范圍內(nèi)。
這個(gè)階段其實(shí)也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠諶egion,時(shí)間是用戶可控制的,所以停頓用戶線程將大幅提高收集效率。
不管是年輕代還是老年代,回收算法主要用的是「復(fù)制算法」,將一個(gè)Region中的存活對(duì)象復(fù)制到另一個(gè)Region中,這種不會(huì)像CMS收集器那樣回收完因?yàn)橛泻芏鄡?nèi)存碎片還需要再整理一次,G1采用復(fù)制算法幾乎不會(huì)有太多內(nèi)存碎片。
注意:CMS回收階段是跟用戶線程一起并發(fā)執(zhí)行的,G1因?yàn)閮?nèi)部實(shí)現(xiàn)太復(fù)雜暫時(shí)沒有實(shí)現(xiàn)并發(fā)回收,不過到了ZGC,Shenandoah就實(shí)現(xiàn)了并發(fā)收集,Shenandoah可以看成是G1的升級(jí)版本。
優(yōu)先列表
G1收集器在后臺(tái)維護(hù)了一個(gè)「優(yōu)先列表」,每次根據(jù)允許的收集時(shí)間,優(yōu)先選擇回收價(jià)值最大的Region,
比如一個(gè)Region花200ms只能回收10M垃圾,另外一個(gè)Region花50ms就能回收20M垃圾,在回收時(shí)間有限的情況下,G1當(dāng)然會(huì)優(yōu)先選擇后面這個(gè)Region來進(jìn)行回收。
這種使用Region劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限時(shí)間內(nèi)可以盡可能的提高收集效率。
G1收集器的優(yōu)點(diǎn):
- 并行與并發(fā):G1能充分利用CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個(gè)CPU核心來縮短 STW 停頓時(shí)間。部分其他收集器原本需要停頓用戶線程來執(zhí)行GC操作,而G1收集器仍然可以通過并發(fā)的方式讓用戶程序繼續(xù)運(yùn)行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆,但還是保留了分代的概念。
- 空間整合:與CMS的「標(biāo)記-清除」算法不同,G1從整體上來看是基于「標(biāo)記-整理」算法實(shí)現(xiàn)的收集器,而從局部上來看是基于「復(fù)制」算法實(shí)現(xiàn)的。
- 可預(yù)測的停頓時(shí)間:這是G1相對(duì)于CMS的另一個(gè)大優(yōu)勢,降低停頓時(shí)間是 G1 和 CMS 共同的關(guān)注點(diǎn),但 G1 除了追求低停頓外,還能建立可預(yù)測的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長度為M毫秒的時(shí)間片段(通過參數(shù)"-XX:MaxGCPauseMillis"指定)內(nèi)完成垃圾收集。
G1垃圾收集分類
Young GC
Young GC并不是說現(xiàn)有的Eden區(qū)放滿了就會(huì)馬上觸發(fā),G1會(huì)計(jì)算現(xiàn)在Eden區(qū)回收大概要多長時(shí)間,如果回收時(shí)間遠(yuǎn)遠(yuǎn)小于參數(shù) -XX:MaxGCPauseMills 設(shè)定的值,那么增加年輕代的Region,繼續(xù)給新對(duì)象存放,不會(huì)馬上做Young GC,直到下次Eden區(qū)放滿,G1計(jì)算回收時(shí)間接近參數(shù) -XX:MaxGCPauseMills 設(shè)定的值,那么就會(huì)觸發(fā) Young GC。
Mixed GC
Mixed GC不是Full GC,老年代的堆占有率達(dá)到參數(shù)(-XX:InitiatingHeapOccupancyPercent)設(shè)定的值時(shí)觸發(fā),回收所有的Young和部分Old(根據(jù)期望的GC停頓時(shí)間確定Old區(qū)垃圾收集的優(yōu)先順序)以及大對(duì)象區(qū),正常情況G1的垃圾收集是先做Mixed GC,主要使用「復(fù)制」算法,需要把各個(gè)Region中存活的對(duì)象拷貝到別的Region中去,拷貝過程中如果發(fā)現(xiàn)沒有足夠的空Region能夠承載拷貝對(duì)象就會(huì)觸發(fā)一次Full GC。
Full GC
停止用戶程序,然后采用單線程進(jìn)行標(biāo)記清理和壓縮整理,使得空閑出來一批Region來供下一次Mixed GC使用,這個(gè)過程是非常耗時(shí)的。
G1收集器參數(shù)設(shè)置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的線程數(shù)量
-XX:G1HeapRegionSize:指定分區(qū)大小(1MB~32MB,且必須是2的N次冪),默認(rèn)將整堆劃分為2048個(gè)分區(qū)
-XX:MaxGCPauseMillis:目標(biāo)暫停時(shí)間(默認(rèn)200ms)
-XX:G1NewSizePercent:新生代內(nèi)存初始空間(默認(rèn)整堆的5%,值配置整數(shù),默認(rèn)就是百分比)
-XX:G1MaxNewSizePercent:新生代最大內(nèi)存空間,默認(rèn)是50%
-XX:InitiatingHeapOccupancyPercent:老年代占用空間達(dá)到整堆內(nèi)存閾值(默認(rèn)45%),則執(zhí)行新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認(rèn)有2048個(gè)Region,如果有接近1000個(gè)Region都是老年代的Region,則可能就要觸發(fā)「Mixed GC」了
-XX:G1MixedGCLiveThresholdPercent:默認(rèn)85%,Region中的存活對(duì)象低于這個(gè)值時(shí)才會(huì)回收該Region,如果超過這個(gè)值,存活對(duì)象過多,回收的的意義不大
-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(默認(rèn)8次),在最后一個(gè)篩選回收階段可以回收一會(huì),然后暫停回收,恢復(fù)用戶程序運(yùn)行,一會(huì)再開始回收,這樣可以讓系統(tǒng)不至于單次停頓時(shí)間過長。
-XX:G1HeapWastePercent:默認(rèn)5%,GC過程中空出來的Region是否充足閾值,在混合回收的時(shí)候,對(duì)Region回收都是基于復(fù)制算法進(jìn)行的,都是把要回收的Region里的存活對(duì)象放入其他Region,然后這個(gè)Region中的垃圾對(duì)象全部清理掉,這樣的話在回收過程中就會(huì)不斷空出來新的Region,一旦空閑出來的Region數(shù)量達(dá)到了整堆內(nèi)存的5%,此時(shí)就會(huì)立即停止「混合回收」,意味著本次混合回收結(jié)束了。
垃圾收集器優(yōu)化建議
假設(shè)參數(shù) -XX:MaxGCPauseMills 設(shè)置的值很大,導(dǎo)致系統(tǒng)運(yùn)行很久,年輕代可能都占用了堆內(nèi)存的60%了,此時(shí)才觸發(fā)年輕代GC。
那么存活下來的對(duì)象可能就會(huì)很多,此時(shí)就會(huì)導(dǎo)致Survivor區(qū)放不下那么多的對(duì)象,就會(huì)進(jìn)入老年代中。
或者是你年輕代GC過后,存活下來的對(duì)象過多,導(dǎo)致進(jìn)入Survivor區(qū)后觸發(fā)了動(dòng)態(tài)年齡判斷機(jī)制,達(dá)到了Survivor區(qū)的50%,也會(huì)導(dǎo)致一些對(duì)象進(jìn)入老年代中。
所以這里的核心還是在于調(diào)節(jié) -XX:MaxGCPauseMills 這個(gè)參數(shù)的值,在保證他的年輕代GC別太頻繁的同時(shí),還得考慮每次GC過后的存活對(duì)象有多少,避免存活對(duì)象太多快速進(jìn)入老年代,導(dǎo)致頻繁觸發(fā)「Mixed GC」。
什么場景適合使用 G1
每秒幾十萬并發(fā)的系統(tǒng)如何優(yōu)化JVM
Kafka類似的支撐高并發(fā)消息系統(tǒng)大家肯定不陌生,對(duì)于kafka來說,每秒處理幾萬甚至幾十萬消息是很正常的,一般來說部署kafka需要用大內(nèi)存機(jī)器(比如64G),也就是說可以給年輕代分配三四十G的內(nèi)存用來支撐高并發(fā)處理,這里就涉及到一個(gè)問題了,我們以前常說的對(duì)于Eden區(qū)的 young GC是很快的,這種大內(nèi)存情況下它的執(zhí)行速度還會(huì)很快嗎?
很顯然不可能,因?yàn)閮?nèi)存太大,處理起來還是要花不少時(shí)間的,假設(shè)三四十G內(nèi)存回收可能最快也要幾秒鐘,按kafka這個(gè)并發(fā)量放滿三四十G的Eden區(qū)可能也就一兩分鐘吧,那么意味著整個(gè)系統(tǒng)每運(yùn)行一兩分鐘就會(huì)因?yàn)閥oung GC卡頓幾秒鐘沒法處理新消息,顯然是不行的。
那么對(duì)于這種情況下如何優(yōu)化呢,我們可以使用G1收集器,設(shè)置 -XX:MaxGCPauseMills 為50ms,假設(shè) 50ms 能夠回收三到四個(gè)G的內(nèi)存,然后50ms的卡頓其實(shí)完全能夠接受,用戶幾乎無感知,那么整個(gè)系統(tǒng)就可以在卡頓幾乎無感知的情況下一邊處理業(yè)務(wù)一邊收集垃圾。
G1天生就適合這種大內(nèi)存機(jī)器的JVM運(yùn)行,可以比較完美的解決大內(nèi)存垃圾回收時(shí)間過長的問題。
如何選擇垃圾收集器
以下有連線的可以搭配使用
JDK1.8 默認(rèn)使用 Parallel(年輕代和老年代都是)
JDK1.9 默認(rèn)使用 G1
16.三色標(biāo)記算法(加分)
為了解決「標(biāo)記-清除」算法的問題,于是就出現(xiàn)了『三色標(biāo)記算法』!
三色標(biāo)記算法指的是將所有對(duì)象分為白色、黑色和灰色三種類型。
黑色表示從 GCRoots 開始,已經(jīng)掃描過它全部引用的對(duì)象,
灰色指的是掃描過對(duì)象本身,還沒完全掃描過它全部引用的對(duì)象,
白色指的是還沒掃描過的對(duì)象。
但僅僅將對(duì)象劃分成三個(gè)顏色還不夠,真正關(guān)鍵的是:實(shí)現(xiàn)可達(dá)性分析算法的時(shí)候,將整個(gè)過程拆分成了初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除四個(gè)階段。
- 初始標(biāo)記階段,指的是標(biāo)記 GC Roots 直接引用的節(jié)點(diǎn),將它們標(biāo)記為灰色,這個(gè)階段需要 「Stop the World」。
- 并發(fā)標(biāo)記階段,指的是從灰色節(jié)點(diǎn)開始,去掃描整個(gè)引用鏈,然后將它們標(biāo)記為黑色,這個(gè)階段不需要「Stop the World」。
- 重新標(biāo)記階段,指的是去校正并發(fā)標(biāo)記階段的錯(cuò)誤,這個(gè)階段需要「Stop the World」。
- 并發(fā)清除,指的是將已經(jīng)確定為垃圾的對(duì)象清除掉,這個(gè)階段不需要「Stop the World」。
通過將最耗時(shí)的引用鏈掃描剝離出來作為「并發(fā)標(biāo)記階段」,將其與用戶線程并發(fā)執(zhí)行,從而極大地降低了 GC 停頓時(shí)間。 但 GC 線程與用戶線程并發(fā)執(zhí)行,會(huì)帶來新的問題:對(duì)象引用關(guān)系可能會(huì)發(fā)生變化,有可能發(fā)生多標(biāo)和漏標(biāo)問題。
多標(biāo)問題
在并發(fā)標(biāo)記過程中,如果由于方法運(yùn)行結(jié)束導(dǎo)致部分局部變量(GC Roots)被銷毀,這個(gè)GC Roots引用的對(duì)象之前又被掃描過(被標(biāo)記為非垃圾對(duì)象),那么本輪GC不會(huì)回收這部分內(nèi)存。
這部分本應(yīng)該回收但沒有回收到的內(nèi)存,被稱之為“浮動(dòng)垃圾”。浮動(dòng)垃圾并不會(huì)影響垃圾回收的正確性,只是需要等到下一輪垃圾回收才被清除。
另外,針對(duì)并發(fā)標(biāo)記(還有并發(fā)清理)開始后產(chǎn)生的新對(duì)象,通常的做法是直接全部當(dāng)成黑色,本輪回收不會(huì)進(jìn)行清除。這部分對(duì)象在運(yùn)行期間可能也會(huì)變成垃圾,這也算是浮動(dòng)垃圾的一部分。
漏標(biāo)問題
漏標(biāo)問題指的是原本應(yīng)該被標(biāo)記為存活的對(duì)象,被遺漏標(biāo)記為白色,從而導(dǎo)致該對(duì)象被錯(cuò)誤地回收。
例如下圖中,假設(shè)我們現(xiàn)在遍歷到了節(jié)點(diǎn) E,此時(shí)應(yīng)用執(zhí)行如下代碼。
這時(shí)候因?yàn)?E 對(duì)象已經(jīng)沒有引用 G 對(duì)象了,因此掃描 E 對(duì)象的時(shí)候并不會(huì)將 G 對(duì)象標(biāo)記為黑色存活狀態(tài)。但由于用戶線程的 D 對(duì)象引用了 G 對(duì)象,這時(shí)候 G 對(duì)象應(yīng)該是存活的,應(yīng)該標(biāo)記為黑色。但由于 D 對(duì)象已經(jīng)被掃描過了,不會(huì)再次掃描,因此 G 對(duì)象就被漏標(biāo)了。
var G = objE.fieldG; objE.fieldG = null; // 灰色E 斷開引用 白色G objD.fieldG = G; // 黑色D 引用 白色G漏標(biāo)問題就非常嚴(yán)重了,其會(huì)導(dǎo)致存活對(duì)象被回收,會(huì)嚴(yán)重影響程序功能。
那么我們的垃圾回收器是怎么解決這個(gè)問題呢?
增加一個(gè)「重新標(biāo)記」階段。無論是在 CMS 回收器還是 G1 回收器,它們都在「并發(fā)標(biāo)記」階段之后,新增了一個(gè)「重新標(biāo)記」階段來校正「并發(fā)標(biāo)記」階段出現(xiàn)的問題。
只是對(duì)于 CMS 回收器和 G1 回收器來說,它們解決的原理不同罷了。
漏標(biāo)解決方案
漏標(biāo)問題要發(fā)生需要滿足如下兩個(gè)充要條件:
CMS 解決方案
CMS 回收器采用的是增量更新方案,即破壞第一個(gè)條件:「有至少一個(gè)黑色對(duì)象在自己被標(biāo)記之后指向了這個(gè)白色對(duì)象」。
既然有黑色對(duì)象在自己標(biāo)記后,又重新指向了白色對(duì)象。那么我們就把這個(gè)新插入的「引用」記錄下來,在后續(xù)「重新標(biāo)記」階段再以這些記錄過的引用關(guān)系中的黑色對(duì)象為根,對(duì)其引用進(jìn)行重新掃描。通過這種方式,被黑色對(duì)象引用的白色對(duì)象就會(huì)變成灰色,從而變?yōu)榇婊顮顟B(tài)。
G1 解決方案
G1 回收器采用的是原始快照的方案,即破壞第二個(gè)條件:「某個(gè)灰色對(duì)象在自己引用掃描完成之前刪除了對(duì)白色對(duì)象的引用」。
既然灰色對(duì)象在掃描完成之前刪除了對(duì)白色對(duì)象的引用,那么我們是否能在灰色對(duì)象取消引用之前,先將這個(gè)要?jiǎng)h除的「引用」記錄下來。隨后在「重新標(biāo)記」階段再以這些記錄過的引用關(guān)系中的灰色對(duì)象為根,對(duì)它的引用進(jìn)行重新掃描,這樣就能掃描到白色對(duì)象,將白色對(duì)象直接標(biāo)記為黑色。
這種方式有個(gè)缺點(diǎn),就是會(huì)產(chǎn)生浮動(dòng)垃圾。 因?yàn)楫?dāng)用戶線程取消引用的時(shí)候,有可能是真的取消引用,對(duì)應(yīng)的對(duì)象是真的要回收掉的。這時(shí)候我們通過這種方式,就會(huì)把本該回收的對(duì)象又復(fù)活了,從而導(dǎo)致出現(xiàn)浮動(dòng)垃圾。但相對(duì)于本該存活的對(duì)象被回收,這個(gè)代價(jià)還是可以接受的,畢竟在下次 GC 的時(shí)候就可以回收了。
為什么G1用原始快照,CMS用增量更新?
原始快照相對(duì)于增量更新效率會(huì)更高(當(dāng)然原始快照可能會(huì)造成更多的浮動(dòng)垃圾),因?yàn)椴恍枰凇?strong>重新標(biāo)記」階段再次深度掃描被刪除引用的對(duì)象,而CMS對(duì)增量引用的根對(duì)象會(huì)做深度掃描;
G1因?yàn)楹芏鄬?duì)象都位于不同的region,CMS就一塊老年代區(qū)域,重新深度掃描根對(duì)象的話G1的代價(jià)會(huì)比CMS高,所以G1選擇原始快照而不深度掃描根對(duì)象,只是簡單標(biāo)記,等到下一輪GC再深度掃描。
寫屏障
以上無論是對(duì)引用關(guān)系的插入還是刪除,虛擬機(jī)的記錄操作都是通過「寫屏障」實(shí)現(xiàn)的
寫屏障
給某個(gè)對(duì)象的成員變量賦值時(shí),其底層代碼大概長這樣:
/** * @param field 某對(duì)象的成員變量,如 a.b.d * @param new_value 新值,如 null */ void oop_field_store(oop* field, oop new_value) { *field = new_value; // 賦值操作 }所謂的寫屏障,其實(shí)就是指在賦值操作前后,加入一些處理(可以參考AOP的概念):
void oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // 寫屏障-寫前操作*field = new_value; post_write_barrier(field, value); // 寫屏障-寫后操作 }寫屏障實(shí)現(xiàn)原始快照
當(dāng)對(duì)象E的成員變量的引用發(fā)生變化時(shí),比如引用消失(e.g = null),我們可以利用寫屏障,將E原來成員變量的引用對(duì)象G記錄下來:
void pre_write_barrier(oop* field) {oop old_value = *field; // 獲取舊值remark_set.add(old_value); // 記錄原來的引用對(duì)象 }寫屏障實(shí)現(xiàn)增量更新
當(dāng)對(duì)象D的成員變量的引用發(fā)生變化時(shí),比如新增引用(d.g = g),我們可以利用寫屏障,將D新的成員變量的引用對(duì)象D記錄下來:
void post_write_barrier(oop* field, oop new_value) { remark_set.add(new_value); // 記錄新引用的對(duì)象 }17.卡表(Card Table)
在新生代做GC Roots可達(dá)性掃描過程中可能會(huì)碰到跨代引用的對(duì)象,這種情況如果又去對(duì)老年代做掃描那效率就太低了。
為此,在新生代可以引入**記憶集(Remember Set)**的數(shù)據(jù)結(jié)構(gòu)(記錄從非收集區(qū)到收集區(qū)的指針集合),避免把整個(gè)老年代加入GC Roots掃描范圍內(nèi)。
在垃圾收集場景中,收集器只需要通過記憶集判斷出某一塊非收集區(qū)域是否存在指向收集區(qū)域的指針即可,無需了解跨代引用指針的全部細(xì)節(jié)。
hotspot使用一種叫做**卡表(Card Table)**的方式實(shí)現(xiàn)記憶集,也是目前最常用的一種方式。關(guān)于卡表與記憶集的關(guān)系, 可以類比Java語言中HashMap與Map的關(guān)系。
卡表使用一個(gè)字節(jié)數(shù)組實(shí)現(xiàn):CARD_TABLE[ ],每個(gè)元素對(duì)應(yīng)著其標(biāo)識(shí)的內(nèi)存區(qū)域一塊特定大小的內(nèi)存塊,稱為“卡頁”。
hotSpot使用的卡頁是2的9次方大小,即512字節(jié)。
將老年代劃分成一塊塊特定大小的區(qū)域(512byte),每一塊稱為一個(gè)card,即卡頁。
一個(gè)卡頁中可包含多個(gè)對(duì)象,只要其中有一個(gè)對(duì)象存在「跨代引用」,其對(duì)應(yīng)的卡表的元素標(biāo)識(shí)就變成1,表示該元素變臟,否則為0。
卡表除了包含對(duì)應(yīng)卡頁的元素標(biāo)識(shí),還存儲(chǔ)了每個(gè)卡頁對(duì)應(yīng)的內(nèi)存地址。
在掃描GC Roots時(shí),除了掃描年輕代的存活對(duì)象之外,還需要掃描卡表中為1的對(duì)應(yīng)卡頁中的對(duì)象。
億級(jí)流量電商系統(tǒng)JVM參數(shù)設(shè)置優(yōu)化
大型電商系統(tǒng)后端現(xiàn)在一般都是拆分成多個(gè)子系統(tǒng)部署的,比如,商品系統(tǒng),庫存系統(tǒng),訂單系統(tǒng),促銷系統(tǒng),會(huì)員系統(tǒng)等等。
這里以比較核心的訂單系統(tǒng)為例:
分析
假設(shè)單臺(tái)機(jī)器每秒產(chǎn)生60M對(duì)象,運(yùn)行14次后占滿Eden區(qū)。
這里需要注意下,前面13秒的所有對(duì)象都可以回收掉,因?yàn)檫@些訂單對(duì)象在產(chǎn)生1秒后都變?yōu)槔鴮?duì)象,而最后一秒產(chǎn)生的對(duì)象(300個(gè)請求),執(zhí)行到一半的時(shí)候Eden區(qū)放滿了,這時(shí)候就會(huì)觸發(fā)Minor GC(STW),意味著最后一秒產(chǎn)生的那60M對(duì)象會(huì)被挪動(dòng)到S區(qū),而前面13秒產(chǎn)生的700多M對(duì)象會(huì)被銷毀。
然后這最后的60M對(duì)象并不會(huì)挪動(dòng)到S區(qū),而是挪動(dòng)到老年代里面去了,這是什么呢?
根據(jù)動(dòng)態(tài)年齡判斷機(jī)制,假設(shè)這60M對(duì)象的年齡都是1,而且光這60M對(duì)象就已經(jīng)超過了S區(qū)內(nèi)存大小的50%,所以會(huì)把這60M對(duì)象已經(jīng)年齡大于等于1的所有對(duì)象都挪動(dòng)到老年代中去。
也就意味著每過14秒就有60M對(duì)象挪動(dòng)到老年代,假設(shè)老年代的空間大小為2G,那么幾分鐘后老年代就會(huì)被放滿,老年代放滿就會(huì)觸發(fā)Full GC,這樣子就會(huì)導(dǎo)致我們的系統(tǒng)幾分鐘就會(huì)執(zhí)行一次Full GC,正常情況下Full GC應(yīng)該是幾個(gè)小時(shí),幾天甚至幾周做一次才正常。
這種情況是可以做優(yōu)化的,只需修改下JVM的參數(shù),就可以讓我們的JVM幾乎不發(fā)生Full GC
解決方案:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8將整個(gè)年輕代調(diào)大一些,比如設(shè)置整個(gè)年輕代為2G,那么Eden區(qū)就是1.6G,S0和S1各為200M。
假設(shè)經(jīng)過25秒后Eden區(qū)才會(huì)放滿,此時(shí)會(huì)觸發(fā)Minor GC,Eden區(qū)會(huì)被清空,而最后的那60M對(duì)象會(huì)被放到S區(qū)中,而根據(jù)動(dòng)態(tài)年齡判斷機(jī)制發(fā)現(xiàn)是沒問題的,因此這60M是可以放進(jìn)S區(qū)的。
再經(jīng)過25秒Eden區(qū)滿了又會(huì)觸發(fā)Minor GC,而此時(shí)會(huì)將S區(qū)的那60M對(duì)象和Eden區(qū)清空,然后再把新的60M對(duì)象放到S區(qū),這樣子循環(huán)反復(fù),我們可以發(fā)現(xiàn)系統(tǒng)不會(huì)觸發(fā)Full GC了。
這樣就降低了因?yàn)閷?duì)象動(dòng)態(tài)年齡判斷機(jī)制導(dǎo)致的對(duì)象頻繁進(jìn)入老年代的問題,其實(shí)很多優(yōu)化無非就是讓短期存活的對(duì)象盡量都留在survivor區(qū)里,不要進(jìn)入老年代,這樣在Minor GC的時(shí)候這些對(duì)象都會(huì)被回收,不會(huì)進(jìn)入到老年代從而導(dǎo)致Full GC。
對(duì)象年齡閾值應(yīng)該設(shè)置為多少才移動(dòng)到老年代比較合適
本例中一次Minor GC要間隔二三十秒,大多數(shù)對(duì)象一般在幾秒內(nèi)就會(huì)變成垃圾,完全可以將默認(rèn)的15歲改小一點(diǎn),比如改為5歲,那么意味著對(duì)象要經(jīng)過5次Minor GC才會(huì)進(jìn)入老年代,整個(gè)時(shí)間也有一兩分鐘了,如果對(duì)象這么長時(shí)間都沒被回收,完全可以認(rèn)為這些對(duì)象是會(huì)存活的比較長的對(duì)象(例如緩存),可以移動(dòng)到老年代,而不是繼續(xù)一直占用survivor區(qū)空間,讓更多對(duì)象可以存放在年輕代。
對(duì)于多大的對(duì)象直接進(jìn)入老年代(參數(shù) -xx:PretenureSizeThreshold),這個(gè)一般可以結(jié)合自己的系統(tǒng)看下有沒有什么大對(duì)象生成,預(yù)估下大對(duì)象的大小,一般來說設(shè)置為1M就差不多了,很少有超過1M的大對(duì)象,這些對(duì)象一般就是你系統(tǒng)初始化分配的緩存對(duì)象,比如大的緩存List,Map之類的對(duì)象。
可以適當(dāng)調(diào)整JVM參數(shù)如下:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M通過ParNew+CMS優(yōu)化(JDK8主流互聯(lián)網(wǎng)公司用到的垃圾收集器組合)
JDK8默認(rèn)的垃圾回收器是 -XX:+UseParallelGC(年輕代)和 -XX:+UseParallelOldGC(老年代),如果內(nèi)存較大(超過4個(gè)G,只是經(jīng)驗(yàn)值),系統(tǒng)對(duì)停頓時(shí)間比較敏感,我們可以使用 ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)
之前做的參數(shù)調(diào)整之后,幾乎不會(huì)再發(fā)生Full GC,但這里有個(gè)前提就是當(dāng)系統(tǒng)壓力維持在每秒1000多單的情況下,才不會(huì)發(fā)生Full GC。
但如果系統(tǒng)在搶購的30分鐘內(nèi),每過幾分鐘就會(huì)有一個(gè)峰值的訪問。峰值的訪問假設(shè)單臺(tái)機(jī)器要承受500單,甚至七八百單、上千單的訪問,在這種情況下就有可能會(huì)出現(xiàn)問題。
在峰值情況下,比如之前是25秒的最后一秒的60M對(duì)象要挪動(dòng)到S區(qū),而在峰值情況下,訂單處理速度是比較慢的(一般單臺(tái)機(jī)器每秒能抗住300單,而如果一下子要抗住七八百單,性能和內(nèi)存會(huì)非常吃緊,會(huì)導(dǎo)致每個(gè)訂單的處理周期變長,一個(gè)訂單的執(zhí)行時(shí)間可能就要跨幾秒鐘),所以有可能在每25秒的最后幾秒內(nèi),這幾秒的對(duì)象都要挪動(dòng)到S區(qū),這些對(duì)象的大小就遠(yuǎn)遠(yuǎn)不止60M了,可能有一兩百M(fèi)甚至兩三百M(fèi),而我們的S區(qū)配置是200M,這樣就放不下這些對(duì)象,所以要將這兩三百M(fèi)對(duì)象直接挪動(dòng)到老年代,這樣有可能在二三十分鐘老年代就滿了,進(jìn)而觸發(fā)Full GC。
然后其實(shí)在半小時(shí)后發(fā)生Full GC,這時(shí)候已經(jīng)過了搶購的最高峰期,后續(xù)可能幾小時(shí)才做一次Full GC。對(duì)于碎片整理,因?yàn)槎际?小時(shí)或幾個(gè)小時(shí)才做一次FullGC,是可以每做完一次就開始碎片整理,或者兩到三次之后再做一次也行。
綜上,只要年輕代參數(shù)設(shè)置合理,老年代CMS的參數(shù)設(shè)置基本都可以用默認(rèn)值,如下所示:
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection // 開啟Full GC后進(jìn)行壓縮整理 -XX:CMSFullGCsBeforeCompaction=3 // 每做完3次Full GC后進(jìn)行一次碎片壓縮整理總結(jié)
以上是生活随笔為你收集整理的一文搞定 JVM 面试,教你吊打面试官~的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试题小结 (数据分析)
- 下一篇: 特种浓缩分离:无机陶瓷膜元件及设备