我們已經(jīng)知道Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,所有對象實例和數(shù)組都在堆上進行內(nèi)存分配。為了進行高效的垃圾回收,虛擬機把堆內(nèi)存劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個區(qū)域。

新生代
新生代由 Eden 與 Survivor Space(S0,S1)構(gòu)成,大小通過-Xmn參數(shù)指定,Eden 與 Survivor Space 的內(nèi)存大小比例默認(rèn)為8:1,可以通過-XX:SurvivorRatio 參數(shù)指定,比如新生代為10M 時,Eden分配8M,S0和S1各分配1M。
Eden:希臘語,意思為伊甸園,在圣經(jīng)中,伊甸園含有樂園的意思,根據(jù)《舊約·創(chuàng)世紀(jì)》記載,上帝耶和華照自己的形像造了第一個男人亞當(dāng),再用亞當(dāng)?shù)囊粋€肋骨創(chuàng)造了一個女人夏娃,并安置他們住在了伊甸園。
大多數(shù)情況下,對象在Eden中分配,當(dāng)Eden沒有足夠空間時,會觸發(fā)一次Minor GC,虛擬機提供了-XX:+PrintGCDetails參數(shù),告訴虛擬機在發(fā)生垃圾回收時打印內(nèi)存回收日志。
Survivor:意思為幸存者,是新生代和老年代的緩沖區(qū)域。
當(dāng)新生代發(fā)生GC(Minor GC)時,會將存活的對象移動到S0內(nèi)存區(qū)域,并清空Eden區(qū)域,當(dāng)再次發(fā)生Minor GC時,將Eden和S0中存活的對象移動到S1內(nèi)存區(qū)域。
存活對象會反復(fù)在S0和S1之間移動,當(dāng)對象從Eden移動到Survivor或者在Survivor之間移動時,對象的GC年齡自動累加,當(dāng)GC年齡超過默認(rèn)閾值15時,會將該對象移動到老年代,可以通過參數(shù)-XX:MaxTenuringThreshold 對GC年齡的閾值進行設(shè)置。
老年代
老年代的空間大小即-Xmx 與-Xmn 兩個參數(shù)之差,用于存放經(jīng)過幾次Minor GC之后依舊存活的對象。當(dāng)老年代的空間不足時,會觸發(fā)Major GC/Full GC,速度一般比Minor GC慢10倍以上。
永久代
在JDK8之前的HotSpot實現(xiàn)中,類的元數(shù)據(jù)如方法數(shù)據(jù)、方法信息(字節(jié)碼,棧和變量大?。⑦\行時常量池、已確定的符號引用和虛方法表等被保存在永久代中,32位默認(rèn)永久代的大小為64M,64位默認(rèn)為85M,可以通過參數(shù)-XX:MaxPermSize進行設(shè)置,一旦類的元數(shù)據(jù)超過了永久代大小,就會拋出OOM異常。
虛擬機團隊在JDK8的HotSpot中,把永久代從Java堆中移除了,并把類的元數(shù)據(jù)直接保存在本地內(nèi)存區(qū)域(堆外內(nèi)存),稱之為元空間。
這樣做有什么好處?
有經(jīng)驗的同學(xué)會發(fā)現(xiàn),對永久代的調(diào)優(yōu)過程非常困難,永久代的大小很難確定,其中涉及到太多因素,如類的總數(shù)、常量池大小和方法數(shù)量等,而且永久代的數(shù)據(jù)可能會隨著每一次Full GC而發(fā)生移動。
而在JDK8中,類的元數(shù)據(jù)保存在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間,可以避免永久代的內(nèi)存溢出問題,不過需要監(jiān)控內(nèi)存的消耗情況,一旦發(fā)生內(nèi)存泄漏,會占用大量的本地內(nèi)存。
ps:JDK7之前的HotSpot,字符串常量池的字符串被存儲在永久代中,因此可能導(dǎo)致一系列的性能問題和內(nèi)存溢出錯誤。在JDK8中,字符串常量池中只保存字符串的引用。
如何判斷對象是否存活
GC動作發(fā)生之前,需要確定堆內(nèi)存中哪些對象是存活的,一般有兩種方法:引用計數(shù)法和可達性分析法。
1、引用計數(shù)法
在對象上添加一個引用計數(shù)器,每當(dāng)有一個對象引用它時,計數(shù)器加1,當(dāng)使用完該對象時,計數(shù)器減1,計數(shù)器值為0的對象表示不可能再被使用。
引用計數(shù)法實現(xiàn)簡單,判定高效,但不能解決對象之間相互引用的問題。
<pre class="brush: java; gutter: true; first-line: 1 hljs" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; word-wrap: break-word; white-space: pre-wrap; font-style: normal; font-variant: normal; font-weight: 400; font-stretch: normal; font-size: 12px; line-height: 20px; font-family: "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class GCtest {
private Object instance = null;
private static final int _10M = 10 * 1 << 20;
// 一個對象占10M,方便在GC日志中看出是否被回收
private byte[] bigSize = new byte[_10M];
public static void main(String[] args) {
GCtest objA = new GCtest();
GCtest objB = new GCtest();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}</pre>
通過添加-XX:+PrintGC參數(shù),運行結(jié)果:
<pre class="brush: java; gutter: true; first-line: 1 hljs json" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; word-wrap: break-word; white-space: pre-wrap; font-style: normal; font-variant: normal; font-weight: 400; font-stretch: normal; font-size: 12px; line-height: 20px; font-family: "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">[GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]</pre>
從GC日志中可以看出objA和objB雖然相互引用,但是它們所占的內(nèi)存還是被垃圾收集器回收了。
2、可達性分析法
通過一系列稱為 “GC Roots” 的對象作為起點,從這些節(jié)點開始向下搜索,搜索路徑稱為 “引用鏈”,以下對象可作為GC Roots:
- 本地變量表中引用的對象
- 方法區(qū)中靜態(tài)變量引用的對象
- 方法區(qū)中常量引用的對象
- Native方法引用的對象
當(dāng)一個對象到 GC Roots 沒有任何引用鏈時,意味著該對象可以被回收。

在可達性分析法中,判定一個對象objA是否可回收,至少要經(jīng)歷兩次標(biāo)記過程:
1、如果對象objA到 GC Roots沒有引用鏈,則進行第一次標(biāo)記。
2、如果對象objA重寫了finalize()方法,且還未執(zhí)行過,那么objA會被插入到F-Queue隊列中,由一個虛擬機自動創(chuàng)建的、低優(yōu)先級的Finalizer線程觸發(fā)其finalize()方法。finalize()方法是對象逃脫死亡的最后機會,GC會對隊列中的對象進行第二次標(biāo)記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯(lián)系,那么在第二次標(biāo)記時,objA會被移出“即將回收”集合。
看看具體實現(xiàn)
<pre class="brush: java; gutter: true; first-line: 1 hljs cs" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; word-wrap: break-word; white-space: pre-wrap; font-style: normal; font-variant: normal; font-weight: 400; font-stretch: normal; font-size: 12px; line-height: 20px; font-family: "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class FinalizerTest {
public static FinalizerTest object;
public void isAlive() {
System.out.println("I'm alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("method finalize is running");
object = this;
}
public static void main(String[] args) throws Exception {
object = new FinalizerTest();
// 第一次執(zhí)行,finalize方法會自救
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
// 第二次執(zhí)行,finalize方法已經(jīng)執(zhí)行過
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
}
}</pre>
執(zhí)行結(jié)果:
<pre class="brush: java; gutter: true; first-line: 1 hljs coffeescript" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; word-wrap: break-word; white-space: pre-wrap; font-style: normal; font-variant: normal; font-weight: 400; font-stretch: normal; font-size: 12px; line-height: 20px; font-family: "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">method finalize is running
I'm alive
I'm dead</pre>
從執(zhí)行結(jié)果可以看出:
第一次發(fā)生GC時,finalize方法的確執(zhí)行了,并且在被回收之前成功逃脫;
第二次發(fā)生GC時,由于finalize方法只會被JVM調(diào)用一次,object被回收。
當(dāng)然了,在實際項目中應(yīng)該盡量避免使用finalize方法。
收集算法
垃圾收集算法主要有:標(biāo)記-清除、復(fù)制和標(biāo)記-整理。
1、標(biāo)記-清除算法
對待回收的對象進行標(biāo)記。
算法缺點:效率問題,標(biāo)記和清除過程效率都很低;空間問題,收集之后會產(chǎn)生大量的內(nèi)存碎片,不利于大對象的分配。
2、復(fù)制算法
復(fù)制算法將可用內(nèi)存劃分成大小相等的兩塊A和B,每次只使用其中一塊,當(dāng)A的內(nèi)存用完了,就把存活的對象復(fù)制到B,并清空A的內(nèi)存,不僅提高了標(biāo)記的效率,因為只需要標(biāo)記存活的對象,同時也避免了內(nèi)存碎片的問題,代價是可用內(nèi)存縮小為原來的一半。
3、標(biāo)記-整理算法
在老年代中,對象存活率較高,復(fù)制算法的效率很低。在標(biāo)記-整理算法中,標(biāo)記出所有存活的對象,并移動到一端,然后直接清理邊界以外的內(nèi)存。
對象標(biāo)記過程
在可達性分析過程中,為了準(zhǔn)確找出與GC Roots相關(guān)聯(lián)的對象,必須要求整個執(zhí)行引擎看起來像是被凍結(jié)在某個時間點上,即暫停所有運行中的線程,不可以出現(xiàn)對象的引用關(guān)系還在不斷變化的情況。
如何快速枚舉GC Roots?
GC Roots主要在全局性的引用(常量或類靜態(tài)屬性)與執(zhí)行上下文(本地變量表中的引用)中,很多應(yīng)用僅僅方法區(qū)就上百兆,如果進行遍歷查找,效率會非常低下。
在HotSpot中,使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)進行實現(xiàn)。類加載完成時,HotSpot把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來存儲到OopMap中,通過JIT編譯出來的本地代碼,也會記錄下棧和寄存器中哪些位置是引用。GC發(fā)生時,通過掃描OopMap的數(shù)據(jù)就可以快速標(biāo)識出存活的對象。
如何安全的GC?
線程運行時,只有在到達安全點(Safe Point)才能停頓下來進行GC。
基于OopMap數(shù)據(jù)結(jié)構(gòu),HotSpot可以快速完成GC Roots的遍歷,不過HotSpot并不會為每條指令都生成對應(yīng)的OopMap,只會在Safe Point處記錄這些信息。
所以Safe Point的選擇很重要,如果太少可能導(dǎo)致GC等待的時間太長,如果太頻繁可能導(dǎo)致運行時的性能問題。大部分指令的執(zhí)行時間都非常短暫,通常會選擇一些執(zhí)行時間較長的指令作為Safe Point,如方法調(diào)用、循環(huán)跳轉(zhuǎn)和異常跳轉(zhuǎn)等。
關(guān)于Safe Point更多的信息,可以看看這篇文章 JVM的Stop The World,安全點,黑暗的地底世界
發(fā)生GC時,如何讓所有線程跑到最近的Safe Point再暫停?
當(dāng)發(fā)生GC時,不直接對線程進行中斷操作,而是簡單的設(shè)置一個中斷標(biāo)志,每個線程運行到Safe Point的時候,主動去輪詢這個中斷標(biāo)志,如果中斷標(biāo)志為真,則將自己進行中斷掛起。
這里忽略了一個問題,當(dāng)發(fā)生GC時,運行中的線程可以跑到Safe Point后進行掛起,而那些處于Sleep或Blocked狀態(tài)的線程在此時無法響應(yīng)JVM的中斷請求,無法到Safe Point處進行掛起,針對這種情況,可以使用安全區(qū)域(Safe Region)進行解決。
Safe Region是指在一段代碼片段中,對象的引用關(guān)系不會發(fā)生變化,在這個區(qū)域中的任何位置開始GC都是安全的。
1、當(dāng)線程運行到Safe Region的代碼時,首先標(biāo)識已經(jīng)進入了Safe Region,如果這段時間內(nèi)發(fā)生GC,JVM會忽略標(biāo)識為Safe Region狀態(tài)的線程;
2、當(dāng)線程即將離開Safe Region時,會檢查JVM是否已經(jīng)完成GC,如果完成了,則繼續(xù)運行,否則線程必須等待直到收到可以安全離開Safe Region的信號為止;
垃圾收集器
Java虛擬機規(guī)范并沒有規(guī)定垃圾收集器應(yīng)該如何實現(xiàn),用戶可以根據(jù)系統(tǒng)特點對各個區(qū)域所使用的收集器進行組合使用。

上圖展示了7種不同分代的收集器,如果兩兩之間存在連線,說明可以組合使用。
1、Serial收集器(串行GC)
Serial 是一個采用單個線程并基于復(fù)制算法工作在新生代的收集器,進行垃圾收集時,必須暫停其他所有的工作線程。對于單CPU環(huán)境來說,Serial由于沒有線程交互的開銷,可以很高效的進行垃圾收集動作,是Client模式下新生代默認(rèn)的收集器。
2、ParNew收集器(并行GC)
ParNew其實是serial的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為與Serial一樣。
3、Parallel Scavenge收集器(并行回收GC)
Parallel Scavenge是一個采用多線程基于復(fù)制算法并工作在新生代的收集器,其關(guān)注點在于達到一個可控的吞吐量,經(jīng)常被稱為“吞吐量優(yōu)先”的收集器。
吞吐量 = 用戶代碼運行時間 /(用戶代碼運行時間 + 垃圾收集時間)
Parallel Scavenge提供了兩個參數(shù)用于精確控制吞吐量:
1、-XX:MaxGCPauseMillis 設(shè)置垃圾收集的最大停頓時間
2、-XX:GCTimeRatio 設(shè)置吞吐量大小
4、Serial Old收集器(串行GC)
Serial Old 是一個采用單線程基于標(biāo)記-整理算法并工作在老年代的收集器,是Client模式下老年代默認(rèn)的收集器。
5、Parallel Old收集器(并行GC)
Parallel Old是一個采用多線程基于標(biāo)記-整理算法并工作在老年代的收集器。在注重吞吐量以及CPU資源敏感的場合,可以優(yōu)先考慮Parallel Scavenge和Parallel Old的收集器組合。
6、CMS收集器(并發(fā)GC)
CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間為目標(biāo)的收集器,工作在老年代,基于“標(biāo)記-清除”算法實現(xiàn),整個過程分為以下4步:
1、初始標(biāo)記:這個過程只是標(biāo)記以下GC Roots能夠直接關(guān)聯(lián)的對象,但是仍然會Stop The World;
2、并發(fā)標(biāo)記:進行GC Roots Tracing的過程,可以和用戶線程一起工作。
3、重新標(biāo)記:用于修正并發(fā)標(biāo)記期間由于用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那部分記錄,這個過程會暫停所有線程,但其停頓時間遠比并發(fā)標(biāo)記的時間短;
4、并發(fā)清理:可以和用戶線程一起工作。
CMS收集器的缺點:
1、對CPU資源比較敏感,在并發(fā)階段,雖然不會導(dǎo)致用戶線程停頓,但是會占用一部分線程資源,降低系統(tǒng)的總吞吐量。
2、無法處理浮動垃圾,在并發(fā)清理階段,用戶線程的運行依然會產(chǎn)生新的垃圾對象,這部分垃圾只能在下一次GC時收集。
3、CMS是基于標(biāo)記-清除算法實現(xiàn)的,意味著收集結(jié)束后會造成大量的內(nèi)存碎片,可能導(dǎo)致出現(xiàn)老年代剩余空間很大,卻無法找到足夠大的連續(xù)空間分配當(dāng)前對象,不得不提前觸發(fā)一次Full GC。
JDK1.5實現(xiàn)中,當(dāng)老年代空間使用率達到68%時,就會觸發(fā)CMS收集器,如果應(yīng)用中老年代增長不是太快,可以通過-XX:CMSInitiatingOccupancyFraction參數(shù)提高觸發(fā)百分比,從而降低內(nèi)存回收次數(shù)提高系統(tǒng)性能。
JDK1.6實現(xiàn)中,觸發(fā)CMS收集器的閾值已經(jīng)提升到92%,要是CMS運行期間預(yù)留的內(nèi)存無法滿足用戶線程需要,會出現(xiàn)一次”Concurrent Mode Failure”失敗,這是虛擬機會啟動Serial Old收集器對老年代進行垃圾收集,當(dāng)然,這樣應(yīng)用的停頓時間就更長了,所以這個閾值也不能設(shè)置的太高,如果導(dǎo)致了”Concurrent Mode Failure”失敗,反而會降低性能,至于如何設(shè)置這個閾值,還得長時間的對老年代空間的使用情況進行監(jiān)控。
7、G1收集器
G1(Garbage First)是JDK1.7提供的一個工作在新生代和老年代的收集器,基于“標(biāo)記-整理”算法實現(xiàn),在收集結(jié)束后可以避免內(nèi)存碎片問題。
G1優(yōu)點:
1、并行與并發(fā):充分利用多CPU來縮短Stop The World的停頓時間;
2、分代收集:不需要其他收集配合就可以管理整個Java堆,采用不同的方式處理新建的對象、已經(jīng)存活一段時間和經(jīng)歷過多次GC的對象獲取更好的收集效果;
3、空間整合:與CMS的”標(biāo)記-清除”算法不同,G1在運行期間不會產(chǎn)生內(nèi)存空間碎片,有利于應(yīng)用的長時間運行,且分配大對象時,不會導(dǎo)致由于無法申請到足夠大的連續(xù)內(nèi)存而提前觸發(fā)一次Full GC;
4、停頓預(yù)測:G1中可以建立可預(yù)測的停頓時間模型,能讓使用者明確指定在M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒。
使用G1收集器時,Java堆的內(nèi)存布局與其他收集器有很大區(qū)別,整個Java堆會被劃分為多個大小相等的獨立區(qū)域Region,新生代和老年代不再是物理隔離了,都是一部分Region(不需要連續(xù))的集合。G1會跟蹤各個Region的垃圾收集情況(回收空間大小和回收消耗的時間),維護一個優(yōu)先列表,根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region,避免在整個Java堆上進行全區(qū)域的垃圾回收,確保了G1收集器可以在有限的時間內(nèi)盡可能收集更多的垃圾。
不過問題來了:使用G1收集器,一個對象分配在某個Region中,可以和Java堆上任意的對象有引用關(guān)系,那么如何判定一個對象是否存活,是否需要掃描整個Java堆?其實這個問題在之前收集器中也存在,如果回收新生代的對象時,不得不同時掃描老年代的話,會大大降低Minor GC的效率。
針對這種情況,虛擬機提供了一個解決方案:G1收集器中Region之間的對象引用關(guān)系和其他收集器中新生代與老年代之間的對象引用關(guān)系被保存在Remenbered Set數(shù)據(jù)結(jié)構(gòu)中,用來避免全堆掃描。G1中每個Region都有一個對應(yīng)的Remenbered Set,當(dāng)虛擬機發(fā)現(xiàn)程序?qū)eference類型的數(shù)據(jù)進行寫操作時,會產(chǎn)生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于相同的Region中,如果不是,則通過CardTable把相關(guān)引用信息記錄到被引用對象所屬Region的Remenbered Set中。
在互聯(lián)網(wǎng)公司面試中,架構(gòu)的底層一定是面試官會問到的問題,針對面試官一般會提到的問題,我錄制了一些分布式,微服務(wù),性能優(yōu)化等技術(shù)點底層原理的錄像視頻,加群619881427可以免費獲取這些錄像,里面還有些分布式,微服務(wù),性能優(yōu)化,Spring,MyBatis的等源碼知識點的錄像視頻。這些視頻都是我找一些資深架構(gòu)師朋友一起錄制出來的,這些視頻幫助以下幾類程序員:
1.對現(xiàn)在的薪資不滿,想要跳槽,卻對自己的技術(shù)沒有信心,不知道如何面對面試官。
2.想從傳統(tǒng)行業(yè)轉(zhuǎn)行到互聯(lián)網(wǎng)行業(yè),但沒有接觸過互聯(lián)網(wǎng)技術(shù)。
3.工作1 - 5年需要提升自己的核心競爭力,但學(xué)習(xí)沒有系統(tǒng)化,不知道自己接下來要學(xué)什么才是正確的,踩坑后又不知道找誰,百度后依然不知所以然。
4.工作5 - 10年無法突破技術(shù)瓶頸(運用過很多技術(shù),在公司一直寫著業(yè)務(wù)代碼,卻依然不懂底層實現(xiàn)原理)
如果你現(xiàn)在正處于我上述所說的幾個階段可以加下我的群來學(xué)習(xí)。而且我也能夠提供一些面試指導(dǎo),職業(yè)規(guī)劃等建議。