大吉大利 :空投十個(gè)JVM核心知識(shí)點(diǎn),速度撿包
想要提高程序員自身的內(nèi)功心法無非就是數(shù)據(jù)結(jié)構(gòu)跟算法 + 操作系統(tǒng) + 計(jì)網(wǎng) + 底層,而所有的Java代碼都是在JVM上運(yùn)行的,了解了JVM好處就是:
寫出更好更健壯的代碼。
提高Java的性能,排除問題。
面試必問,要對(duì)知識(shí)有一定對(duì)深度。
1、簡述JVM 內(nèi)存模型

從宏觀上來說JVM 內(nèi)存區(qū)域 分為三部分線程共享區(qū)域、線程私有區(qū)域、直接內(nèi)存區(qū)域(上圖static變量該在堆區(qū))。
1.1、線程共享區(qū)域
1.1.1、堆區(qū)
堆區(qū)Heap是JVM中最大的一塊內(nèi)存區(qū)域,基本上所有的對(duì)象實(shí)例都是在堆上分配空間。堆區(qū)細(xì)分為年輕代和老年代,其中年輕代又分為Eden、S0、S1 三個(gè)部分,他們默認(rèn)的比例是8:1:1的大小。
1.1.1、元空間
方法區(qū):
在 《Java虛擬機(jī)規(guī)范》中只是規(guī)定了有 方法區(qū)這么個(gè)概念跟它的作用。HotSpot在JDK8之前 搞了個(gè)永久代把這個(gè)概念實(shí)現(xiàn)了。用來主要存儲(chǔ)類信息、常量池、靜態(tài)變量、JIT編譯后的代碼等數(shù)據(jù)。PermGen(永久代)中類的元數(shù)據(jù)信息在每次FullGC的時(shí)候可能會(huì)被收集,但成績很難令人滿意。而且為PermGen分配多大的空間因?yàn)榇鎯?chǔ)上述多種數(shù)據(jù)很難確定大小。因此官方在JDK8剔除移除永久代。
官方解釋移除永久代:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation. 即:移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因?yàn)镴Rockit沒有永久代,不需要配置永久代。
元空間:
在Java中用
永久代來存儲(chǔ)類信息,常量,靜態(tài)變量等數(shù)據(jù)不是好辦法,因?yàn)檫@樣很容易造成內(nèi)存溢出。同時(shí)對(duì)永久代的性能調(diào)優(yōu)也很困難,因此在JDK8中 把永久代去除了,引入了元空間metaspace,原先的class、field等變量放入到metaspace。
總結(jié):
元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,但可以通過參數(shù)來指定元空間的大小。
1.2、直接內(nèi)存區(qū)域
直接內(nèi)存:
一般使用Native函數(shù)操作C++代碼來實(shí)現(xiàn)直接分配堆外內(nèi)存,不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。這塊內(nèi)存不受Java堆空間大小的限制,但是受本機(jī)總內(nèi)存大小限制所以也會(huì)出現(xiàn)OOM異常。分配空間后避免了在Java堆區(qū)跟Native堆中來回復(fù)制數(shù)據(jù),可以有效提高讀寫效率,但它的創(chuàng)建、銷毀卻比普通Buffer慢。
PS:如果使用了NIO,本地內(nèi)存區(qū)域會(huì)被頻繁的使用,此時(shí) jvm內(nèi)存 ≈ 方法區(qū) + 堆 + 棧+ 直接內(nèi)存
1.3、線程私有區(qū)域
程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧跟線程的聲明周期是一樣的。
1.3.1、程序計(jì)數(shù)器
課堂上比如你正在看小說《誅仙》,看到1412章節(jié)時(shí),老師喊你回答問題,這個(gè)時(shí)候你肯定要先應(yīng)付老師的問題,回答完畢后繼續(xù)接著看,這個(gè)時(shí)候你可以用書簽也可以憑借記憶記住自己在看的位置,通過這樣實(shí)現(xiàn)繼續(xù)閱讀。
落實(shí)到代碼運(yùn)行時(shí)候同樣道理,程序計(jì)數(shù)器用于記錄當(dāng)前線程下虛擬機(jī)正在執(zhí)行的字節(jié)碼的指令地址。它具有如下特性:
線程私有
多線程情況下,在同一時(shí)刻所以為了讓線程切換后依然能恢復(fù)到原位,每條線程都需要有各自獨(dú)立的程序計(jì)數(shù)器。
沒有規(guī)定OutOfMemoryError
程序計(jì)數(shù)器存儲(chǔ)的是字節(jié)碼文件的行號(hào),而這個(gè)范圍是可知曉的,在一開始分配內(nèi)存時(shí)就可以分配一個(gè)絕對(duì)不會(huì)溢出的內(nèi)存。
執(zhí)行Native方法時(shí)值為空
Native方法大多是通過C實(shí)現(xiàn)并未編譯成需要執(zhí)行的字節(jié)碼指令,也就不需要去存儲(chǔ)字節(jié)碼文件的行號(hào)了。
1.3.2、虛擬機(jī)棧
方法的出入棧:調(diào)用的方法會(huì)被打包成棧楨,一個(gè)棧楨至少需要包含一個(gè)局部變量表、操作數(shù)棧、楨數(shù)據(jù)區(qū)、動(dòng)態(tài)鏈接。

動(dòng)態(tài)鏈接:
當(dāng)棧幀內(nèi)部包含一個(gè)指向運(yùn)行時(shí)常量池引用前提下,類加載時(shí)候會(huì)進(jìn)行符號(hào)引用到直接引用的解析跟鏈接替換。
局部變量表:
局部變量表是棧幀重要組中部分之一。他主要保存函數(shù)的參數(shù)以及局部的變量信息。局部變量表中的變量作用域是當(dāng)前調(diào)用的函數(shù)。函數(shù)調(diào)用結(jié)束后,隨著函數(shù)棧幀的銷毀。局部變量表也會(huì)隨之銷毀,釋放空間。
操作數(shù)棧:
保存著Java虛擬機(jī)執(zhí)行過程中數(shù)據(jù)
方法返回地址:
方法被調(diào)用的位置,當(dāng)方法退出時(shí)候?qū)嶋H上等同于當(dāng)前棧幀出棧。
比如執(zhí)行簡單加減法:
public class ShowByteCode {
private String xx;
private static final int TEST = 1;
public ShowByteCode() {
}
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
}
執(zhí)行javap -c *.class:
1.3.3、本地方法棧
跟虛擬機(jī)棧類似,只是為使用到的Native方法服務(wù)而已。
2、判斷對(duì)象是否存活
JVM空間不夠就需要Garbage Collection了,一般共享區(qū)的都要被回收比如堆區(qū)以及方法區(qū)。在進(jìn)行內(nèi)存回收之前要做的事情就是判斷那些對(duì)象是死的,哪些是活的。常用方法有兩種 引用計(jì)數(shù)法 跟 可達(dá)性分析。
2.1、引用計(jì)數(shù)法
思路是給 Java 對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器 +1;引用失效則 -1,當(dāng)計(jì)數(shù)器不為 0 時(shí),判斷該對(duì)象存活;否則判斷為死亡(計(jì)數(shù)器 = 0)。優(yōu)點(diǎn):
實(shí)現(xiàn)簡單,判斷高效。
缺點(diǎn):
無法解決 對(duì)象間 相互循環(huán)引用 的問題
class GcObject {
public Object instance = null;
}
public class GcDemo {
public static void main(String[] args) {
GcObject object1 = new GcObject(); // step 1
GcObject object2 = new GcObject(); // step 2
object1.instance = object2 ;//step 3
object2.instance = object1; //step 4
object1 = null; //step 5
object2 = null; // step 6
}
}
step1: GcObject實(shí)例1的引用計(jì)數(shù)+1,實(shí)例1引用數(shù) = 1
step2: GcObject實(shí)例2的引用計(jì)數(shù)+1,實(shí)例2引用數(shù) = 1
step3: GcObject實(shí)例2的引用計(jì)數(shù)+1,實(shí)例2引用數(shù) = 2
step4: GcObject實(shí)例1的引用計(jì)數(shù)+1,實(shí)例1引用數(shù) = 2
step5: GcObject實(shí)例1的引用計(jì)數(shù)-1,結(jié)果為 1
step6: GcObject實(shí)例2的引用計(jì)數(shù)-1,結(jié)果為 1
如上分析發(fā)現(xiàn)實(shí)例1跟實(shí)例2的引用數(shù)都不為0而又相互引用,這兩個(gè)實(shí)例所占有的內(nèi)存則無法釋放。
2.2、可達(dá)性分析
很多主流商用語言(如Java、C#)都采用引用鏈法判斷對(duì)象是否存活,大致的思路就是將一系列的 GC Roots 對(duì)象作為起點(diǎn),從這些起點(diǎn)開始向下搜索。在Java語言中,可作為 GC Roots 的對(duì)象包含以下幾種:
第一種是虛擬機(jī)棧中的引用的對(duì)象,在程序中正常創(chuàng)建一個(gè)對(duì)象,對(duì)象會(huì)在堆上開辟一塊空間,同時(shí)會(huì)將這塊空間的地址作為引用保存到虛擬機(jī)棧中,如果對(duì)象生命周期結(jié)束了,那么引用就會(huì)從虛擬機(jī)棧中出棧,因此如果在虛擬機(jī)棧中有引用,就說明這個(gè)對(duì)象還是有用的,這種情況是最常見的。
第二種是我們在類中定義了全局的靜態(tài)的對(duì)象,也就是使用了
static關(guān)鍵字,由于虛擬機(jī)棧是線程私有的,所以這種對(duì)象的引用會(huì)保存在共有的方法區(qū)中,顯然將方法區(qū)中的靜態(tài)引用作為GC Roots是必須的。第三種便是常量引用,就是使用了
static final關(guān)鍵字,由于這種引用初始化之后不會(huì)修改,所以方法區(qū)常量池里的引用的對(duì)象也應(yīng)該作為GC Roots。第四種是在使用JNI技術(shù)時(shí),有時(shí)候單純的Java代碼并不能滿足我們的需求,我們可能需要在Java中調(diào)用C或C++的代碼,因此會(huì)使用Native方法,JVM內(nèi)存中專門有一塊本地方法棧,用來保存這些對(duì)象的引用,所以本地方法棧中引用的對(duì)象也會(huì)被作為GC Roots。
GC Root步驟主要包含如下三步:
2.1.1 可達(dá)性分析

當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí),則判斷該對(duì)象不可達(dá)。注意: 可達(dá)性分析僅僅只是判斷對(duì)象是否可達(dá),但還不足以判斷對(duì)象是否存活 / 死亡。
2.1.2 第一次標(biāo)記 & 篩選
篩選的條件對(duì)象 如果沒有重寫finalize或者調(diào)用過finalize 則將該對(duì)象加入到F-Queue中
2.1.3 第二次標(biāo)記 & 篩選
當(dāng)對(duì)象經(jīng)過了第一次的標(biāo)記 & 篩選,會(huì)被進(jìn)行第二次標(biāo)記 & 準(zhǔn)備被進(jìn)行篩選。經(jīng)過F-Queue篩選后如果對(duì)象還沒有跟GC Root建立引用關(guān)系則被回收,屬于給個(gè)二次機(jī)會(huì)。

2.3、四大引用類型
2.3.1 強(qiáng)引用
強(qiáng)引用(StrongReference)是使用最普遍的引用。垃圾回收器絕對(duì)不會(huì)回收它,內(nèi)存不足時(shí)寧愿拋出OOM導(dǎo)致程序異常,平常的new 對(duì)象就是。
2.3.2 軟引用
垃圾回收器在內(nèi)存充足時(shí)不會(huì)回收軟引用(SoftReference)對(duì)象,不足時(shí)會(huì)回收它,特別適合用于創(chuàng)建緩存。
2.3.3 弱引用
弱引用(WeakReference)是在掃描到該對(duì)象時(shí)無論內(nèi)存是否充足都會(huì)回收該對(duì)象。ThreadLocal 的Key就是弱引用。
2.3.4 虛引用
如果一個(gè)對(duì)象只具有虛引用(PhantomReference)那么跟沒有任何引用一樣,任何適合都可以被回收。主要用跟蹤對(duì)象跟垃圾回收器回收的活動(dòng)。
3、垃圾回收算法
為了揮手回收垃圾操作系統(tǒng)一般會(huì)使用標(biāo)記清除、復(fù)制算法、標(biāo)記整理三種算法,這三種各有優(yōu)劣,簡單介紹下:
3.1、標(biāo)記清除

原理:
算法分為
標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
缺點(diǎn):
標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致觸發(fā)GC。
3.2、標(biāo)記復(fù)制

原理:
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
缺點(diǎn):
這種算法的代價(jià)是將內(nèi)存縮小為了原來的一半,還要來回移動(dòng)數(shù)據(jù)。
3.3、標(biāo)記整理

原理:
首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后,后續(xù)步驟是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
缺點(diǎn):
涉及到移動(dòng)大量對(duì)象,效率不高。
總結(jié):
| 指標(biāo) | 標(biāo)記清理 | 標(biāo)記整理 | 標(biāo)記復(fù)制 |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 快 |
| 空間開銷 | 少(但會(huì)堆積碎片) | 少(不堆積碎片) | 通常需要活對(duì)象的2倍大?。ú欢逊e碎片) |
| 移動(dòng)對(duì)象 | 否 | 是 | 是 |
3.4 、三色標(biāo)記跟讀寫屏障
前面說的三種回收算法都說到了先標(biāo)記,問題是如何標(biāo)記的呢?說話說一半,小心沒老伴!
接下來的知識(shí)點(diǎn)個(gè)人感覺面試應(yīng)該問不到那么深了,但是為了裝逼必須Mark下!CMS、G1 標(biāo)記時(shí)候一般用的是三色標(biāo)記法,根據(jù)可達(dá)性分析從GC Roots開始進(jìn)行遍歷訪問,可達(dá)的則為存活對(duì)象,而最終不可達(dá)說明就是需要被GC對(duì)象。大致流程是把遍歷對(duì)象圖過程中遇到的對(duì)象,按是否訪問過這個(gè)條件標(biāo)記成以下三種顏色:
白色:尚未訪問過。
黑色:本對(duì)象已訪問過,而且本對(duì)象 引用到 的其他對(duì)象 也全部訪問過了。灰色:本對(duì)象已訪問過,但是本對(duì)象 引用到 的其他對(duì)象 尚未全部訪問完。全部訪問后會(huì)轉(zhuǎn)換為黑色。

假設(shè)現(xiàn)在有白、灰、黑三個(gè)集合(表示當(dāng)前對(duì)象的顏色),遍歷訪問過程:
1、初始時(shí)所有對(duì)象都在白色集合中。
2、將GC Roots 直接引用到的對(duì)象挪到灰色集合中。
3、從灰色集合中獲取對(duì)象:第一步將本對(duì)象 引用到的 其他對(duì)象 全部挪到灰色集合中,第二步將本對(duì)象 挪到黑色集合里面。
4、重復(fù)步驟3,直至灰色集合為空時(shí)結(jié)束。
5、結(jié)束后仍在白色集合的對(duì)象即為GC Roots 不可達(dá),可以嘗試進(jìn)行回收。
當(dāng)STW時(shí)對(duì)象間的引用是不會(huì)發(fā)生變化的,可以輕松完成標(biāo)記。當(dāng)支持并發(fā)標(biāo)記時(shí),對(duì)象間的引用可能發(fā)生變化,多標(biāo)和漏標(biāo)的情況就有可能發(fā)生。
3.4 .1、浮動(dòng)垃圾
狀況:GC線程遍歷到E(E是灰色),一個(gè)業(yè)務(wù)線程執(zhí)行了D.E = null,此時(shí)E應(yīng)該被回收的。但是GC線程已經(jīng)認(rèn)為E是灰色了會(huì)繼續(xù)遍歷,導(dǎo)致E沒有被回收。

3.4 .2、漏標(biāo)

GC線程遍歷到E(灰色了)。業(yè)務(wù)線程執(zhí)行了E-->G斷開,D-->G鏈接的操作。GC線程發(fā)現(xiàn)E無法到達(dá)G,因?yàn)槭呛谏粫?huì)再遍歷標(biāo)記了。最終導(dǎo)致漏標(biāo)G。漏標(biāo)的必備兩個(gè)條件:灰到白斷開,黑到白建立。
Object G = E.G; // 第一步 :讀
Object E.G = null; // 第二步:寫
Object D.G = G; // 第三步:寫
漏標(biāo)解決方法:
將對(duì)象G存儲(chǔ)到特定集合中,等并發(fā)標(biāo)記遍歷完畢后再對(duì)集合中對(duì)象進(jìn)行
重新標(biāo)記。
3.4.2.1、CMS方案
這里比如開始B指向C,但是后來B不指向C,A指向D,最簡單的方法是將A變成灰色,等待下次進(jìn)行再次遍歷。CMS中可能引發(fā)ABA問題:

1、回收線程 m1 正在標(biāo)記A,屬性A.1標(biāo)記完畢,正在標(biāo)記屬性A.2。
2、業(yè)務(wù)線程 m2 把屬性1指向了C,由于CMS方案此時(shí)回收線程 m3 把A標(biāo)記位灰色。
3、回收線程 m1 認(rèn)為所有屬性標(biāo)記完畢,將A設(shè)置為黑色,結(jié)果C漏標(biāo)。所以CMS階段需要重新標(biāo)記。

3.4.2.2、讀寫屏障
漏標(biāo)的實(shí)現(xiàn)是有三步的,JVM加入了讀寫屏障,其中讀屏障則是攔截第一步,寫屏障用于攔截第二和第三步。
寫屏障 + SATB(原始快照) 來破壞 灰到白斷開。
寫屏障 + 增量更新 來破壞 黑到白建立。
讀屏障 一種保守方式來破壞灰到白斷開后白的存儲(chǔ),此時(shí)用讀屏障OK的。
現(xiàn)代使用可達(dá)性分析的垃圾回收器幾乎都借鑒了三色標(biāo)記的算法思想,盡管實(shí)現(xiàn)的方式不盡相同。對(duì)于讀寫屏障,以Java HotSpot VM為例,其并發(fā)標(biāo)記時(shí)對(duì)漏標(biāo)的處理方案如下:
CMS:寫屏障 + 增量更新
G1:寫屏障 + SATB
ZGC:讀屏障
CMS中使用的增量更新,在重新標(biāo)記階段除了需要遍歷 寫屏障的記錄,還需要重新掃描遍歷GC Roots(標(biāo)記過的不用再標(biāo)記),這是由于CMS對(duì)于astore_x等指令不添加寫屏障的原因。
4、GC流程
核心思想就是根據(jù)各個(gè)年代的特點(diǎn)不同選用不同到垃圾收集算法。
年輕代:使用復(fù)制算法老年代:使用標(biāo)記整理或者標(biāo)記清除算法。
為什么要有年輕代:
分代的好處就是
優(yōu)化GC性能,如果沒有分代每次掃描所有區(qū)域能累死GC。因?yàn)楹芏鄬?duì)象幾乎就是朝生夕死的,如果分代的話,我們把新創(chuàng)建的對(duì)象放到某一地方,當(dāng)GC的時(shí)候先把這塊存朝生夕死(80%以上)對(duì)象的區(qū)域進(jìn)行回收,這樣就會(huì)騰出很大的空間出來。
4.1、 年輕代
HotSpot JVM把年輕代分為了三部分:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫from和to)。默認(rèn)比例為8:1:1。一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(一些大對(duì)象特殊處理),這些對(duì)象經(jīng)過第一次Minor GC后,如果仍然存活,將會(huì)被移到Survivor區(qū)。對(duì)象在Survivor區(qū)中每熬過一次Minor GC年齡就會(huì)增加1歲,當(dāng)它的年齡增加到一定次數(shù)(默認(rèn)15次)時(shí),就會(huì)被移動(dòng)到年老代中。年輕代的垃圾回收算法使用的是復(fù)制算法。

年輕代GC過程:GC開始前,年輕代對(duì)象只會(huì)存在于Eden區(qū)和名為From的Survivor區(qū),名為To的Survivor區(qū)永遠(yuǎn)是空的。如果新分配對(duì)象在Eden申請(qǐng)空間發(fā)現(xiàn)不足就會(huì)導(dǎo)致GC。
yang GC:Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到To,而在From區(qū)中,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到To區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,From和To會(huì)交換他們的角色,也就是新的To就是上次GC前的From,新的From就是上次GC前的To。不管怎樣都會(huì)保證名為To的Survivor區(qū)域是空的。Minor GC會(huì)一直重復(fù)這樣的過程,直到To區(qū)被填滿,To區(qū)被填滿之后,會(huì)將所有對(duì)象移動(dòng)到年老代中。這里注意如果yang GC 后空間還是不夠用則會(huì) 空間擔(dān)保 機(jī)制將數(shù)據(jù)送到Old區(qū)
卡表 Card Table:
為了支持高頻率的新生代回收,虛擬機(jī)使用一種叫做 卡表(Card Table)的數(shù)據(jù)結(jié)構(gòu),卡表作為一個(gè)比特位的集合,每一個(gè)比特位可以用來表示年老代的某一區(qū)域中的所有對(duì)象是否持有新生代對(duì)象的引用。新生代GC時(shí)不用花大量的時(shí)間掃描所有年老代對(duì)象,來確定每一個(gè)對(duì)象的引用關(guān)系,先掃描卡表,只有卡表的標(biāo)記位為1時(shí),才需要掃描給定區(qū)域的年老代對(duì)象。而卡表位為0的所在區(qū)域的年老代對(duì)象,一定不包含有對(duì)新生代的引用。
4.2、 老年代
老年代GC過程:
老年代中存放的對(duì)象是存活了很久的,年齡大于15的對(duì)象 或者 觸發(fā)了老年代的
分配擔(dān)保機(jī)制存儲(chǔ)的大對(duì)象。在老年代觸發(fā)的gc叫major gc也叫full gc。full gc會(huì)包含年輕代的gc。full gc采用的是 標(biāo)記-清除 或 標(biāo)記整理。在執(zhí)行full gc的情況下,會(huì)阻塞程序的正常運(yùn)行。老年代的gc比年輕代的gc效率上慢10倍以上。對(duì)效率有很大的影響。所以一定要盡量避免老年代GC!
4.3、 元空間
永久代的回收會(huì)隨著full gc進(jìn)行移動(dòng),消耗性能。每種類型的垃圾回收都需要特殊處理 元數(shù)據(jù)。將元數(shù)據(jù)剝離出來,簡化了垃圾收集,提高了效率。
-XX:MetaspaceSize 初始空間的大小。達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)GC會(huì)對(duì)該值進(jìn)行調(diào)整:
如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值。
-XX:MaxMetaspaceSize:
最大空間,默認(rèn)是沒有限制的。
4.4 、垃圾回收流程總結(jié)

大致的GC回收流程如上圖,還有一種設(shè)置就是 大對(duì)象直接進(jìn)入老年代:
如果在新生代分配失敗且對(duì)象是一個(gè)不含任何對(duì)象引用的大數(shù)組,可被直接分配到老年代。通過在老年代的分配避免新生代的一次垃圾回收。 設(shè)置了-XX:PretenureSizeThreshold 值,任何比這個(gè)值大的對(duì)象都不會(huì)嘗試在新生代分配,將在老年代分配內(nèi)存。
內(nèi)存回收跟分配策略
優(yōu)先在Eden上分配對(duì)象,此區(qū)域垃圾回收頻繁速度還快。 大對(duì)象直接進(jìn)入老生代。 年長者(長期存活對(duì)象默認(rèn)15次)跟 進(jìn)入老生代。 在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象會(huì)群體進(jìn)入老生代。 空間分配擔(dān)保(擔(dān)保minorGC),如果Minor GC后 Survivor區(qū)放不下新生代仍存活的對(duì)象,把Suvivor 無法容納的對(duì)象直接進(jìn)人老年代。
5、垃圾收集器
5.1、 垃圾收集器
堆heap是垃圾回收機(jī)制的重點(diǎn)區(qū)域。我們知道垃圾回收機(jī)制有三種minor gc、major gc 和full gc。針對(duì)于堆的就是前兩種。年輕代的叫 minor gc,老年代的叫major gc。
JDK7、JDK8 默認(rèn)垃圾收集器 Parallel Scavenge(新生代)+ Parallel Old(老年代)JDK9 默認(rèn)垃圾收集器 G1服務(wù)端開發(fā)常見組合就是 ParNew + CMS
工程化使用的時(shí)候使用指定的垃圾收集器組合使用,講解垃圾收集器前先普及幾個(gè)重要知識(shí)點(diǎn):
STW
java中
Stop-The-World機(jī)制簡稱STW,是指執(zhí)行垃圾收集算法時(shí)Java應(yīng)用程序的其他所有線程都被掛起(除了垃圾收集幫助器之外)。是Java中一種全局暫停現(xiàn)象,全局停頓,所有Java代碼停止,native代碼雖然可以執(zhí)行但不能與JVM交互,如果發(fā)生了STW 現(xiàn)象多半是由于gc引起。
吞吐量
吞吐量 = 運(yùn)行用戶代碼時(shí)間 / ( 運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間 )。例如:虛擬機(jī)共運(yùn)行100分鐘,垃圾收集器花掉1分鐘,那么吞吐量就是99%
垃圾收集時(shí)間
垃圾回收頻率 * 單次垃圾回收時(shí)間
并行收集
指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍處于等待狀態(tài)。
并發(fā)收集
指用戶線程與垃圾收集線程同時(shí)工作(不一定是并行的可能會(huì)交替執(zhí)行)。用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行在另一個(gè)CPU上。
5.2、 新生代
新生代有Serial、ParNew、Parallel Scavenge三種垃圾收集器。
| 名稱 | 串行/并行/并發(fā) | 回收算法 | 使用場(chǎng)景 | 可以跟CMS配合 |
|---|---|---|---|---|
| Serial | 串行 | 復(fù)制 | 單CPU,Client模式下虛擬機(jī) | 是 |
| ParNew | 并行(Serial的并行版) | 復(fù)制 | 多CPU,常在Server模式 | 是 |
| Parallel Scavenge | 并行 | 復(fù)制 | 多CPU且關(guān)注吞吐量 | 否 |
5.3、 老年代
老年代有Serial Old、Parallel Old、CMS 三種垃圾收集器。
| 名稱 | 串行/并行/并發(fā) | 回收算法 | 使用場(chǎng)景 | 組合年輕代 |
|---|---|---|---|---|
| Serial Old | 串行 | 標(biāo)記整理 | 單CPU | Serial 、ParNew、Parallel Scavenge |
| Parallel Old | 并行 | 標(biāo)記整理 | 多CPU | Parallel Scavenge |
| CMS | 并發(fā) | 標(biāo)記清除 | 多CPU且關(guān)注吞吐量,常用Server端 | Serial 、ParNew |
5.3.1、CMS
CMS(Concurrent Mark Sweep)比較重要這里 重點(diǎn)說一下。
CMS的初衷和目的:
為了消除Throught收集器和Serial收集器在Full GC周期中的長時(shí)間停頓。是一種
以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,具有自適應(yīng)調(diào)整策略,適合互聯(lián)網(wǎng)站 跟B/S 服務(wù)應(yīng)用。
CMS的適用場(chǎng)景:
如果你的應(yīng)用需要更快的響應(yīng),不希望有長時(shí)間的停頓,同時(shí)你的CPU資源也比較豐富,就適合適用CMS收集器。比如常見的Server端任務(wù)。
優(yōu)點(diǎn):
并發(fā)收集、低停頓。
缺點(diǎn):
CMS收集器對(duì)CPU資源非常敏感:在并發(fā)階段,雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)占用CPU資源而導(dǎo)致引用程序變慢,總吞吐量下降。無法處理浮動(dòng)垃圾:由于CMS并發(fā)清理階段用戶線程還在運(yùn)行,伴隨程序的運(yùn)行自熱會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時(shí)將其清理掉。這一部分垃圾稱為浮動(dòng)垃圾。如果內(nèi)存放不下浮動(dòng)垃圾這時(shí) JVM 啟動(dòng) Serial Old 替代 CMS。空間碎片:CMS是基于標(biāo)記-清除算法實(shí)現(xiàn)的收集器,使用標(biāo)記-清除算法收集后,會(huì)產(chǎn)生大量碎片。
CMS回收流程:
初始標(biāo)記:引發(fā)STW, 僅僅只是標(biāo)記出GC ROOTS能直接關(guān)聯(lián)到的對(duì)象,速度很快。并發(fā)標(biāo)記:不引發(fā)STW,正常運(yùn)行 所有Old 對(duì)象是否可鏈到GC Roots重新標(biāo)記:引發(fā)STW,為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生改變的標(biāo)記。這個(gè)階段的停頓時(shí)間會(huì)被初始標(biāo)記階段稍長,但比并發(fā)標(biāo)記階段要短。并發(fā)清除:不引發(fā)STW,正常運(yùn)行,標(biāo)記清除算法來清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象。
總結(jié):
并發(fā)標(biāo)記和并發(fā)清除的耗時(shí)最長但是不需要停止用戶線程。初始標(biāo)記和重新標(biāo)記的耗時(shí)較短,但是需要停止用戶線程,所以整個(gè)GC過程造成的停頓時(shí)間較短,大部分時(shí)候是可以和用戶線程一起工作的。
5.4、G1
之前的GC收集器對(duì)Heap的劃分:
以前垃圾回收器是 新生代 + 老年代,用了CMS效果也不是很好,為了減少STW對(duì)系統(tǒng)的影響引入了G1(Garbage-First Garbage Collector),G1是一款面向服務(wù)端應(yīng)用的垃圾收集器,具有如下特點(diǎn):
1、
并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢(shì),可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。2、
分代收集:分代概念在G1中依然得以保留,它能夠采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過多次GC的舊對(duì)象來獲得更好的收集效果。3、
空間整合:G1從整體上看是基于標(biāo)記-整理算法實(shí)現(xiàn)的,從局部(兩個(gè)Region之間)上看是基于復(fù)制算法實(shí)現(xiàn)的,G1運(yùn)行期間不會(huì)產(chǎn)生內(nèi)存空間碎片。4、
可預(yù)測(cè)停頓:G1比CMS牛在能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒。
G1作為JDK9之后的服務(wù)端默認(rèn)收集器,不再區(qū)分年輕代和老年代進(jìn)行垃圾回收,G1默認(rèn)把堆內(nèi)存分為N個(gè)分區(qū),每個(gè)1~32M(總是2的冪次方)。并且提供了四種不同Region標(biāo)簽Eden、Survivor 、Old、 Humongous。H區(qū)可以認(rèn)為是Old區(qū)中一種特列專門用來存儲(chǔ)大數(shù)據(jù)的,關(guān)于H區(qū)數(shù)據(jù)存儲(chǔ)類型一般符合下面條件:
當(dāng) 0.5 Region <= 當(dāng)對(duì)象大小 <= 1 Region 時(shí)候?qū)?shù)據(jù)存儲(chǔ)到 H區(qū)
當(dāng)對(duì)象大小 > 1 Region 存儲(chǔ)到連續(xù)的H區(qū)。
同時(shí)G1中引入了RememberSets、CollectionSets幫助更好的執(zhí)行GC 。
1、
RememberSets: RSet 記錄了其他Region中的對(duì)象引用本Region中對(duì)象的關(guān)系,屬于points-into結(jié)構(gòu)(誰引用了我的對(duì)象)2、
CollectionSets:Csets 是一次GC中需要被清理的regions集合,注意G1每次GC不是全部region都參與的,可能只清理少數(shù)幾個(gè),這幾個(gè)就被叫做Csets。在GC的時(shí)候,對(duì)于old -> young 和old -> old的跨代對(duì)象引用,只要掃描對(duì)應(yīng)的CSet中的RSet即可。
G1進(jìn)行GC的時(shí)候一般分為Yang GC跟Mixed GC。
Young GC:CSet 就是所有年輕代里面的Region
Mixed GC:CSet 是所有年輕代里的Region加上在全局并發(fā)標(biāo)記階段標(biāo)記出來的收益高的Region
5.4.1、Yang GC
標(biāo)準(zhǔn)的年輕代GC算法,整體思路跟CMS中類似。
5.4.2、Mixed GC
G1中是沒有Old GC的,有一個(gè)把老年代跟新生代同時(shí)GC的 Mixed GC,它的回收流程:
1、
初始標(biāo)記:是STW事件,其完成工作是標(biāo)記GC ROOTS 直接可達(dá)的對(duì)象。標(biāo)記位RootRegion。2、
根區(qū)域掃描:不是STW事件,拿來RootRegion,掃描整個(gè)Old區(qū)所有Region,看每個(gè)Region的Rset中是否有RootRegion。有則標(biāo)識(shí)出來。3、
并發(fā)標(biāo)記:同CMS并發(fā)標(biāo)記 不需要STW,遍歷范圍減少,在此只需要遍歷 第二步 被標(biāo)記到引用老年代的對(duì)象 RSet。4、
最終標(biāo)記:同 CMS 重新標(biāo)記 會(huì)STW ,用的SATB操作,速度更快。5、
清除:STW操作,用 復(fù)制清理算法,清點(diǎn)出有存活對(duì)象的Region和沒有存活對(duì)象的Region(Empty Region),更新Rset。把Empty Region收集起來到可分配Region隊(duì)列。
回收總結(jié):
1、經(jīng)過global concurrent marking,collector就知道哪些Region有存活的對(duì)象。并將那些完全可回收的Region(沒有存活對(duì)象)收集起來加入到可分配Region隊(duì)列,實(shí)現(xiàn)對(duì)該部分內(nèi)存的回收。對(duì)于有存活對(duì)象的Region,G1會(huì)根據(jù)統(tǒng)計(jì)模型找處收益最高、開銷不超過用戶指定的上限的若干Region進(jìn)行對(duì)象回收。這些選中被回收的Region組成的集合就叫做collection set 簡稱Cset!
2、在MIX GC中的Cset = 所有年輕代里的region + 根據(jù)global concurrent marking統(tǒng)計(jì)得出收集收益高的若干old region。
3、在YGC中的Cset = 所有年輕代里的region + 通過控制年輕代的region個(gè)數(shù)來控制young GC的開銷。
4、YGC 與 MIXGC 都是采用多線程復(fù)制清理,整個(gè)過程會(huì)STW。G1的低延遲原理在于其回收的區(qū)域變得精確并且范圍變小了。
G1提速點(diǎn):
1 重新標(biāo)記時(shí)X區(qū)域直接刪除。
2 Rset降低了掃描的范圍,上題中兩點(diǎn)。
3 重新標(biāo)記階段使用SATB速度比CMS快。
4 清理過程為選取部分存活率低的Region進(jìn)行清理,不是全部,提高了清理的效率。
總結(jié):
就像你媽讓你把自己臥室打掃干凈,你可能只把顯眼而比較大的垃圾打掃了,犄角旮旯的你沒打掃。關(guān)于G1 還有很多細(xì)節(jié)其實(shí)沒看到也。
一句話總結(jié)G1思維:
每次選擇性的清理大部分垃圾來保證時(shí)效性跟系統(tǒng)的正常運(yùn)行。
6、New個(gè)對(duì)象
一個(gè)Java類從編碼到最終完成執(zhí)行,主要包括兩個(gè)過程,編譯、運(yùn)行。
編譯:將我們寫好的.java文件通過Javac命令編譯成.class文件。
運(yùn)行:把編譯生成的.class文件交由JVM執(zhí)行。
Jvm運(yùn)行class類的時(shí)候,并不是一次性將所有的類都加載到內(nèi)存中,而是用到哪個(gè)就加載哪個(gè),并且只加載一次。
6.1、類的生命周期

6.1.1、 加載
加載指的是把class字節(jié)碼文件從各個(gè)來源通過類加載器裝載入內(nèi)存中,這里有兩個(gè)重點(diǎn):
字節(jié)碼來源:一般的加載來源包括從本地路徑下編譯生成的.class文件,從jar包中的.class文件,從遠(yuǎn)程網(wǎng)絡(luò),以及動(dòng)態(tài)代理實(shí)時(shí)編譯 類加載器:一般包括啟動(dòng)類加載器,擴(kuò)展類加載器,應(yīng)用類加載器,以及用戶的自定義類加載器(加密解密那種)。
6.1.2、 驗(yàn)證
主要是為了保證加載進(jìn)來的字節(jié)流符合虛擬機(jī)規(guī)范,不會(huì)造成安全錯(cuò)誤。文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。
6.1.3、 準(zhǔn)備
給類靜態(tài)變量分配內(nèi)存空間,僅僅是分配空間,比如 public static int age = 14,在準(zhǔn)備后age = 0,在初始化階段 age = 14,如果添加了final則在這個(gè)階段直接賦值為14。
6.1.4、 解析
將常量池內(nèi)的符號(hào)引用替換為直接引用。

6.1.5、 初始化
前面在加載類階段用戶應(yīng)用程序可以通過自定義類加載器參與之外 其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。此時(shí)才是真正開始執(zhí)行類中定義的代碼 :執(zhí)行static代碼塊進(jìn)行初始化,如果存在父類,先對(duì)父類進(jìn)行初始化。
6.1.6、 使用
類加載完畢后緊接著就是為對(duì)象分配內(nèi)存空間和初始化了:
為對(duì)象分配合適大小的內(nèi)存空間 為實(shí)例變量賦默認(rèn)值 設(shè)置對(duì)象的頭信息,對(duì)象hash碼、GC分代年齡、元數(shù)據(jù)信息等 執(zhí)行構(gòu)造函數(shù)(init)初始化。
6.1.7、 卸載
最終沒啥說等,就是通過GC算法回收對(duì)象了。
6.2、 對(duì)象占據(jù)字節(jié)
關(guān)于對(duì)象頭問題在 Synchronized 一文中已經(jīng)詳細(xì)寫過了,一個(gè)對(duì)象頭包含三部分對(duì)象頭(MarkWord、classPointer)、實(shí)例數(shù)據(jù)Instance Data、對(duì)齊Padding,想看內(nèi)存詳細(xì)占用情況IDEA調(diào)用jol-core包即可。
問題一:new Object()占多少字節(jié)
markword 8字節(jié) + classpointer 4字節(jié)(默認(rèn)用calssPointer壓縮) + padding 4字節(jié) = 16字節(jié) 如果沒開啟classpointer壓縮:markword 8字節(jié) + classpointer 8字節(jié) = 16字節(jié)
問題二:User (int id,String name) User u = new User(1,"李四")
markword 8字節(jié) + 開啟classPointer壓縮后classpointer 4字節(jié) + instance data int 4字節(jié) + 開啟普通對(duì)象指針壓縮后String4字節(jié) + padding 4 = 24字節(jié)
6.3、 對(duì)象訪問方式

使用句柄:
使用句柄來訪問的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。
直接指針:
reference中存儲(chǔ)的直接就是對(duì)象地址。最大好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項(xiàng)非??捎^的執(zhí)行成本。
Sun HotSpot 使用 直接指針訪問方式 進(jìn)行對(duì)象訪問的。
7、對(duì)象一定創(chuàng)建在堆上嗎
結(jié)論:不一定 看對(duì)象經(jīng)過了逃逸分析后發(fā)現(xiàn)該變量只是用到方法區(qū)時(shí),則JVM會(huì)自動(dòng)優(yōu)化,在棧上創(chuàng)建該對(duì)象。
7.1、逃逸分析
逃逸分析(Escape Analysis)簡單來講就是:Java Hotspot 虛擬機(jī)可以分析新創(chuàng)建對(duì)象的使用范圍,并決定是否在 Java 堆上分配內(nèi)存。
7.2、標(biāo)量替換
標(biāo)量替換:JVM通過逃逸分析確定該對(duì)象不會(huì)被外部訪問。那就通過將該對(duì)象標(biāo)量替換分解在棧上分配內(nèi)存,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
標(biāo)量:不可被進(jìn)一步分解的量,而JAVA的基本數(shù)據(jù)類型就是標(biāo)量
聚合量:在JAVA中對(duì)象就是可以被進(jìn)一步分解的聚合量。
7.3、棧上分配
JVM對(duì)象分配在堆中,當(dāng)對(duì)象沒有被引用時(shí),依靠GC進(jìn)行回收內(nèi)存,如果對(duì)象數(shù)量較多會(huì)給GC帶來較大壓力,也間接影響了應(yīng)用的性能。
為了減少臨時(shí)對(duì)象在堆內(nèi)分配的數(shù)量,JVM通過逃逸分析確定該對(duì)象不會(huì)被外部訪問。那就通過將該對(duì)象標(biāo)量替換分解在棧上分配內(nèi)存,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
7.4、同步消除
同步消除是java虛擬機(jī)提供的一種優(yōu)化技術(shù)。通過逃逸分析,可以確定一個(gè)對(duì)象是否會(huì)被其他線程進(jìn)行訪問,如果對(duì)象沒有出現(xiàn)線程逃逸,那該對(duì)象的讀寫就不會(huì)存在資源的競(jìng)爭(zhēng),不存在資源的競(jìng)爭(zhēng),則可以消除對(duì)該對(duì)象的同步鎖。比如方法體內(nèi)調(diào)用StringBuffer。
逃逸分析結(jié)論:
雖然經(jīng)過逃逸分析可以做
標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過程。如果對(duì)象經(jīng)過層層分析后發(fā)現(xiàn) 無法進(jìn)行逃逸分析優(yōu)化則反而耗時(shí)了,因此慎用。
8、類加載器
在連接階段一般是無法干預(yù)的,大部分干預(yù) 類加載階段,對(duì)于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性,類加載時(shí)候重要三個(gè)方法:
1、loadClass() :加載目標(biāo)類的入口,它首先會(huì)查找當(dāng)前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標(biāo)類,找到直接返回
2、findClass() :如果沒有找到就會(huì)讓雙親嘗試加載,如果雙親都加載不了,就會(huì)調(diào)用 findClass() 讓自定義加載器自己來加載目標(biāo)類
3、defineClass() :拿到這個(gè)字節(jié)碼之后再調(diào)用 defineClass() 方法將字節(jié)碼轉(zhuǎn)換成 Class 對(duì)象。
8.1、雙親委派機(jī)制

定義:
當(dāng)某個(gè)類加載器需要加載某個(gè).class文件時(shí),首先把這個(gè)任務(wù)委托給他的上級(jí)類加載器,遞歸這個(gè)操作,如果上級(jí)的類加載器沒有加載,自己才會(huì)去加載這個(gè)類。
作用:
1、可以防止重復(fù)加載同一個(gè).class。通過委托去向上面問一問,加載過了,就不用再加載一遍。保證數(shù)據(jù)安全。
2、保證核心.class不能被篡改*,通過委托方式,不會(huì)去篡改核心.class?!?/p>
類加載器:
1、BootstrapClassLoader(啟動(dòng)類加載器):c++編寫,加載java核心庫 java.*,JAVA_HOME/lib
2、ExtClassLoader (標(biāo)準(zhǔn)擴(kuò)展類加載器):java編寫的加載擴(kuò)展庫,JAVA_HOME/lib/ext
3、AppClassLoader(系統(tǒng)類加載器):加載程序所在的目錄,如user.dir所在的位置的ClassPath
4、CustomClassLoader(用戶自定義類加載器):用戶自定義的類加載器,可加載指定路徑的class文件
8.2、關(guān)于加載機(jī)制
雙親委派機(jī)制只是Java類加載的一種常見模式,還有別的加載機(jī)制哦,比如Tomcat 總是先嘗試去加載某個(gè)類,如果找不到再用上一級(jí)的加載器,跟雙親加載器順序正好相反。再比如當(dāng)使用第三方框架JDBC跟具體實(shí)現(xiàn)的時(shí)候,反而會(huì)引發(fā)錯(cuò)誤,因?yàn)镴DK自帶的JDBC接口由啟動(dòng)類加載,而第三方實(shí)現(xiàn)接口由應(yīng)用類加載。這樣相互之間是不認(rèn)識(shí)的,因此JDK引入了SPI機(jī)制 線程上下文加載器 來實(shí)現(xiàn)加載(跟Dubbo的SPI不一樣哦)。
9、OOM 、CPU100%
系統(tǒng)性能分析常用指令:
| 工具 | 用途 |
|---|---|
| jps | 輸出JVM中運(yùn)行的進(jìn)程狀態(tài)信息 |
| jstack | 生成虛擬機(jī)當(dāng)前時(shí)刻的線程快照 |
| jstat | 虛擬機(jī)統(tǒng)計(jì)信息監(jiān)控工具 |
| jinfo | 實(shí)時(shí)地查看和調(diào)整虛擬機(jī)各項(xiàng)參數(shù) |
| jmap | 生成虛擬機(jī)的內(nèi)存轉(zhuǎn)儲(chǔ)快照,heapdump文件 |
| JConsole | 可視化管理工具,常用 |
9.1、OOM
9.1.1、為啥OOM
發(fā)生 OOM 簡單來說可總結(jié)為兩個(gè)原因:
分配給 JVM的 內(nèi)存不夠用。 分配內(nèi)存夠用,但代碼寫的不好,多余的內(nèi)存 沒有釋放,導(dǎo)致內(nèi)存不夠用。
9.1.2、三種類型OOM
9.2.1、堆內(nèi)存溢出:
此種情況最常見 Java heap space。一般是先通過內(nèi)存映像工具對(duì)Dump出來的堆轉(zhuǎn)儲(chǔ)快照然后辨別到底是內(nèi)存泄漏還是內(nèi)存溢出。內(nèi)存泄漏
通過工具查看泄漏對(duì)象到GC Roots的引用鏈。找到泄漏的對(duì)象是通過怎么樣的路徑與GC Roots相關(guān)聯(lián)的導(dǎo)致垃圾回收機(jī)制無法將其回收,最終比較準(zhǔn)確地定位泄漏代碼的位置。
不存在泄漏
就是內(nèi)存中的對(duì)象確實(shí)必須存活著,那么此時(shí)就需要通過虛擬機(jī)的堆參數(shù),從代碼上檢查是否存在某些對(duì)象存活時(shí)間過長、持有時(shí)間過長的情況,嘗試減少運(yùn)行時(shí)內(nèi)存的消耗。
9.2.2、虛擬機(jī)棧和本地方法棧溢出
在HotSpot虛擬機(jī)上不區(qū)分虛擬機(jī)棧和本地方法棧,因此棧容量只能由**-Xss**參數(shù)設(shè)定。在Java虛擬機(jī)規(guī)范中描述了兩種異常:
StackOverflowError :線程請(qǐng)求的棧深度超過了虛擬機(jī)所允許的最大深度,就會(huì)拋出該異常。OutOfMemoryError:虛擬機(jī)在拓展棧的時(shí)候無法申請(qǐng)到足夠的空間,就會(huì)拋出該異常。
單線程環(huán)境下無論是由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法繼續(xù)分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError 異常。
多線程環(huán)境下為每個(gè)線程的棧分配的內(nèi)存越大,每個(gè)線程獲得空間大則可建立的線程數(shù)減少了反而越容易產(chǎn)生OOM異常,因此一般通過減少最大堆 和 減少棧容量 來換取更多的線程數(shù)量。
9.2.3、永久代溢出:
PermGen space 即方法區(qū)溢出了。方法區(qū)用于存放Class的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。當(dāng)前的一些主流框架,如Spring、Hibernate,對(duì)于類進(jìn)行增強(qiáng)的時(shí)候都會(huì)使用到CGLib這類字節(jié)碼技術(shù),增強(qiáng)的類越多,就需要越大的方法區(qū)來保證動(dòng)態(tài)生成Class可以加載入內(nèi)存,這樣的情況下可能會(huì)造成方法區(qū)的OOM異常。
9.2.4、OOM查看指令
通過命令查看對(duì)應(yīng)的進(jìn)程號(hào) :
比如:jps 或者 ps -ef | grep 需要的任務(wù)
輸入命令查看gc情況命令:
jstat -gcutil 進(jìn)程號(hào) 刷新的毫秒數(shù) 展示的記錄數(shù)
比如:jstat -gcutil 1412 1000 10 (查看進(jìn)程號(hào)1412,每隔1秒獲取下,展示10條記錄)
查看具體占用情況:
命令:jmap -histo 進(jìn)程號(hào) | more (默認(rèn)展示到控制臺(tái))
比如:jmap -histo 1412 | more 查看具體的classname,是否有開發(fā)人員的類,也可以輸出到具體文件分析
9.3 CPU 100%
線上應(yīng)用導(dǎo)致 CPU 占用 100%, 出現(xiàn)這樣問題一般情況下是代碼進(jìn)入了死循環(huán),分析步驟如下:
找出對(duì)應(yīng)服務(wù)進(jìn)程id :
用 ps -ef | grep 運(yùn)行的服務(wù)名字,直接top命令也可以看到各個(gè)進(jìn)程CPU使用情況。
查詢目標(biāo)進(jìn)程下所有線程的運(yùn)行情況 :
top -Hp pid, -H表示以線程的維度展示,默認(rèn)以進(jìn)程維度展示。
對(duì)目標(biāo)線程進(jìn)行10進(jìn)制到16進(jìn)制轉(zhuǎn)換:
printf ‘%x\n’ 線程pid
用jstack 進(jìn)程id | grep 16進(jìn)制線程id 找到線程信息,具體分析:
jstack 進(jìn)程ID | grep -A 20 16進(jìn)制線程id
10、GC調(diào)優(yōu)
一般項(xiàng)目加個(gè)xms和xmx參數(shù)就夠了。在沒有全面監(jiān)控、收集性能數(shù)據(jù)之前,調(diào)優(yōu)就是瞎調(diào)。出現(xiàn)了問題先看自身代碼或者參數(shù)是否不合理,畢竟不是誰都能寫JVM底層代碼的。一般要減少創(chuàng)建對(duì)象的數(shù)量, 減少使用全局變量和大對(duì)象, GC 優(yōu)化是到最后不得已才采用的手段。日常 分析 GC 情況 優(yōu)化代碼比優(yōu)化 GC 參數(shù)要多得多。一般如下情況不用調(diào)優(yōu)的:
1、minor GC 單次耗時(shí) < 50ms,頻率10秒以上。說明年輕代OK。
2、Full GC 單次耗時(shí) < 1秒,頻率10分鐘以上,說明年老代OK。
GC調(diào)優(yōu)目的:GC時(shí)間夠少,GC次數(shù)夠少。
調(diào)優(yōu)建議:
-Xms5m設(shè)置JVM初始堆為5M,-Xmx5m設(shè)置JVM最大堆為5M。-Xms跟-Xmx值一樣時(shí)可以避免每次垃圾回收完成后JVM重新分配內(nèi)存。 -Xmn2g:設(shè)置年輕代大小為2G,一般默認(rèn)為整個(gè)堆區(qū)的1/3 ~ 1/4。-Xss每個(gè)線程??臻g設(shè)置。 -XX:SurvivorRatio,設(shè)置年輕代中Eden區(qū)與Survivor區(qū)的比值,默認(rèn)=8,比值為8:1:1。 -XX:+HeapDumpOnOutOfMemoryError 當(dāng)JVM發(fā)生OOM時(shí),自動(dòng)生成DUMP文件。 -XX:PretenureSizeThreshold 當(dāng)創(chuàng)建的對(duì)象超過指定大小時(shí),直接把對(duì)象分配在老年代。 -XX:MaxTenuringThreshold 設(shè)定對(duì)象在Survivor區(qū)最大年齡閾值,超過閾值轉(zhuǎn)移到老年代,默認(rèn)15。 開啟GC日志對(duì)性能影響很小且能幫助我們定位問題,-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log
JVM思維導(dǎo)圖:https://www.processon.com/view/link/5e69db12e4b055496ae4a673 三色標(biāo)記法與讀寫屏障:https://blog.csdn.net/qq_21383435/article/details/106311542 JVM調(diào)優(yōu)參數(shù)陷阱:https://hllvm-group.iteye.com/group/topic/27945
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取