大吉大利 :空投十個JVM核心知識點(diǎn),速度撿包

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

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

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

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

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

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

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

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

假設(shè)現(xiàn)在有白、灰、黑三個集合(表示當(dāng)前對象的顏色),遍歷訪問過程:
1、初始時所有對象都在白色集合中。
2、將GC Roots 直接引用到的對象挪到灰色集合中。?
3、從灰色集合中獲取對象:第一步將本對象 引用到的 其他對象 全部挪到灰色集合中,第二步將本對象 挪到黑色集合里面。?
4、重復(fù)步驟3,直至灰色集合為空時結(jié)束。?
5、結(jié)束后仍在白色集合的對象即為GC Roots 不可達(dá),可以嘗試進(jìn)行回收。
當(dāng)STW時對象間的引用是不會發(fā)生變化的,可以輕松完成標(biāo)記。當(dāng)支持并發(fā)標(biāo)記時,對象間的引用可能發(fā)生變化,多標(biāo)和漏標(biāo)的情況就有可能發(fā)生。
3.4 .1、浮動垃圾
狀況:GC線程遍歷到E(E是灰色),一個業(yè)務(wù)線程執(zhí)行了D.E = null,此時E應(yīng)該被回收的。但是GC線程已經(jīng)認(rèn)為E是灰色了會繼續(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)槭呛谏粫俦闅v標(biāo)記了。最終導(dǎo)致漏標(biāo)G。漏標(biāo)的必備兩個條件:灰到白斷開,黑到白建立。
Object?G?=?E.G;????//?第一步?:讀
Object?E.G?=?null;?//?第二步:寫
Object?D.G?=?G;???//?第三步:寫
漏標(biāo)解決方法:
將對象G存儲到特定集合中,等并發(fā)標(biāo)記遍歷完畢后再對集合中對象進(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方案此時回收線程 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(原始快照) 來破壞 灰到白斷開。?
寫屏障 + 增量更新 來破壞 黑到白建立。?
讀屏障 一種保守方式來破壞灰到白斷開后白的存儲,此時用讀屏障OK的。
現(xiàn)代使用可達(dá)性分析的垃圾回收器幾乎都借鑒了三色標(biāo)記的算法思想,盡管實(shí)現(xiàn)的方式不盡相同。對于讀寫屏障,以Java HotSpot VM為例,其并發(fā)標(biāo)記時對漏標(biāo)的處理方案如下:
CMS:寫屏障 + 增量更新
G1:寫屏障 + SATB
ZGC:讀屏障
CMS中使用的增量更新,在重新標(biāo)記階段除了需要遍歷 寫屏障的記錄,還需要重新掃描遍歷GC Roots(標(biāo)記過的不用再標(biāo)記),這是由于CMS對于astore_x等指令不添加寫屏障的原因。
4、GC流程
核心思想就是根據(jù)各個年代的特點(diǎn)不同選用不同到垃圾收集算法。
年輕代:使用復(fù)制算法老年代:使用標(biāo)記整理或者標(biāo)記清除算法。
為什么要有年輕代:
分代的好處就是
優(yōu)化GC性能,如果沒有分代每次掃描所有區(qū)域能累死GC。因?yàn)楹芏鄬ο髱缀蹙褪?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">朝生夕死的,如果分代的話,我們把新創(chuàng)建的對象放到某一地方,當(dāng)GC的時候先把這塊存朝生夕死(80%以上)對象的區(qū)域進(jìn)行回收,這樣就會騰出很大的空間出來。
4.1、 年輕代
HotSpot JVM把年輕代分為了三部分:1個Eden區(qū)和2個Survivor區(qū)(分別叫from和to)。默認(rèn)比例為8:1:1。一般情況下,新創(chuàng)建的對象都會被分配到Eden區(qū)(一些大對象特殊處理),這些對象經(jīng)過第一次Minor GC后,如果仍然存活,將會被移到Survivor區(qū)。對象在Survivor區(qū)中每熬過一次Minor GC年齡就會增加1歲,當(dāng)它的年齡增加到一定次數(shù)(默認(rèn)15次)時,就會被移動到年老代中。年輕代的垃圾回收算法使用的是復(fù)制算法。

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

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

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

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

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

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