我們已經(jīng)知道Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,所有對象實例和數(shù)組都在堆上進行內(nèi)存分配你雌。為了進行高效的垃圾回收,虛擬機把堆內(nèi)存劃分成新生代(Young Generation)氓栈、老年代(Old Generation)和永久代(Permanent Generation)3個區(qū)域授瘦。
新生代
新生代由 Eden 與 Survivor Space(S0,S1)構成氯葬,大小通過-Xmn參數(shù)指定,Eden 與 Survivor Space 的內(nèi)存大小比例默認為8:1闯睹,可以通過-XX:SurvivorRatio 參數(shù)指定担神,比如新生代為10M 時楼吃,Eden分配8M,S0和S1各分配1M。
Eden:希臘語孩锡,意思為伊甸園酷宵,在圣經(jīng)中,伊甸園含有樂園的意思躬窜,根據(jù)《舊約·創(chuàng)世紀》記載浇垦,上帝耶和華照自己的形像造了第一個男人亞當,再用亞當?shù)囊粋€肋骨創(chuàng)造了一個女人夏娃荣挨,并安置他們住在了伊甸園此虑。
大多數(shù)情況下,對象在Eden中分配,當Eden沒有足夠空間時,會觸發(fā)一次Minor GC,虛擬機提供了-XX:+PrintGCDetails參數(shù),告訴虛擬機在發(fā)生垃圾回收時打印內(nèi)存回收日志。
Survivor:意思為幸存者,是新生代和老年代的緩沖區(qū)域。
當新生代發(fā)生GC(Minor GC)時,會將存活的對象移動到S0內(nèi)存區(qū)域,并清空Eden區(qū)域,當再次發(fā)生Minor GC時,將Eden和S0中存活的對象移動到S1內(nèi)存區(qū)域。
存活對象會反復在S0和S1之間移動涧卵,當對象從Eden移動到Survivor或者在Survivor之間移動時乐设,對象的GC年齡自動累加戈锻,當GC年齡超過默認閾值15時,會將該對象移動到老年代,可以通過參數(shù)-XX:MaxTenuringThreshold 對GC年齡的閾值進行設置。
老年代
老年代的空間大小即-Xmx 與-Xmn 兩個參數(shù)之差懒震,用于存放經(jīng)過幾次Minor GC之后依舊存活的對象娘香。當老年代的空間不足時安接,會觸發(fā)Major GC/Full GC,速度一般比Minor GC慢10倍以上额获。
永久代
在JDK8之前的HotSpot實現(xiàn)中境肾,類的元數(shù)據(jù)如方法數(shù)據(jù)纯趋、方法信息(字節(jié)碼,棧和變量大谐渍 )、運行時常量池缚去、已確定的符號引用和虛方法表等被保存在永久代中,32位默認永久代的大小為64M,64位默認為85M,可以通過參數(shù)-XX:MaxPermSize進行設置剔蹋,一旦類的元數(shù)據(jù)超過了永久代大小买优,就會拋出OOM異常娇钱。
虛擬機團隊在JDK8的HotSpot中常挚,把永久代從Java堆中移除了酱床,并把類的元數(shù)據(jù)直接保存在本地內(nèi)存區(qū)域(堆外內(nèi)存),稱之為元空間囊蓝。
這樣做有什么好處汇鞭?
有經(jīng)驗的同學會發(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,字符串常量池的字符串被存儲在永久代中齿诞,因此可能導致一系列的性能問題和內(nèi)存溢出錯誤酸休。在JDK8中,字符串常量池中只保存字符串的引用祷杈。
如何判斷對象是否存活
GC動作發(fā)生之前斑司,需要確定堆內(nèi)存中哪些對象是存活的,一般有兩種方法:引用計數(shù)法和可達性分析法但汞。
1宿刮、引用計數(shù)法
在對象上添加一個引用計數(shù)器互站,每當有一個對象引用它時,計數(shù)器加1僵缺,當使用完該對象時胡桃,計數(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ù),運行結果:
<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方法引用的對象
當一個對象到 GC Roots 沒有任何引用鏈時龟糕,意味著該對象可以被回收桐磁。
在可達性分析法中,判定一個對象objA是否可回收讲岁,至少要經(jīng)歷兩次標記過程:
1我擂、如果對象objA到 GC Roots沒有引用鏈,則進行第一次標記缓艳。
2校摩、如果對象objA重寫了finalize()方法,且還未執(zhí)行過阶淘,那么objA會被插入到F-Queue隊列中衙吩,由一個虛擬機自動創(chuàng)建的、低優(yōu)先級的Finalizer線程觸發(fā)其finalize()方法溪窒。finalize()方法是對象逃脫死亡的最后機會坤塞,GC會對隊列中的對象進行第二次標記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯(lián)系澈蚌,那么在第二次標記時摹芙,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í)行結果:
<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í)行結果可以看出:
第一次發(fā)生GC時,finalize方法的確執(zhí)行了,并且在被回收之前成功逃脫盈电;
第二次發(fā)生GC時蝴簇,由于finalize方法只會被JVM調(diào)用一次,object被回收挣轨。
當然了,在實際項目中應該盡量避免使用finalize方法轩猩。
收集算法
垃圾收集算法主要有:標記-清除卷扮、復制和標記-整理。
1均践、標記-清除算法
對待回收的對象進行標記晤锹。
算法缺點:效率問題,標記和清除過程效率都很低彤委;空間問題鞭铆,收集之后會產(chǎn)生大量的內(nèi)存碎片,不利于大對象的分配焦影。
2车遂、復制算法
復制算法將可用內(nèi)存劃分成大小相等的兩塊A和B,每次只使用其中一塊斯辰,當A的內(nèi)存用完了舶担,就把存活的對象復制到B,并清空A的內(nèi)存彬呻,不僅提高了標記的效率衣陶,因為只需要標記存活的對象,同時也避免了內(nèi)存碎片的問題闸氮,代價是可用內(nèi)存縮小為原來的一半剪况。
3、標記-整理算法
在老年代中蒲跨,對象存活率較高译断,復制算法的效率很低。在標記-整理算法中或悲,標記出所有存活的對象镐作,并移動到一端,然后直接清理邊界以外的內(nèi)存隆箩。
對象標記過程
在可達性分析過程中该贾,為了準確找出與GC Roots相關聯(lián)的對象,必須要求整個執(zhí)行引擎看起來像是被凍結在某個時間點上捌臊,即暫停所有運行中的線程杨蛋,不可以出現(xiàn)對象的引用關系還在不斷變化的情況。
如何快速枚舉GC Roots?
GC Roots主要在全局性的引用(常量或類靜態(tài)屬性)與執(zhí)行上下文(本地變量表中的引用)中逞力,很多應用僅僅方法區(qū)就上百兆曙寡,如果進行遍歷查找,效率會非常低下寇荧。
在HotSpot中举庶,使用一組稱為OopMap的數(shù)據(jù)結構進行實現(xiàn)。類加載完成時揩抡,HotSpot把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來存儲到OopMap中户侥,通過JIT編譯出來的本地代碼,也會記錄下棧和寄存器中哪些位置是引用峦嗤。GC發(fā)生時蕊唐,通過掃描OopMap的數(shù)據(jù)就可以快速標識出存活的對象。
如何安全的GC烁设?
線程運行時替梨,只有在到達安全點(Safe Point)才能停頓下來進行GC。
基于OopMap數(shù)據(jù)結構装黑,HotSpot可以快速完成GC Roots的遍歷副瀑,不過HotSpot并不會為每條指令都生成對應的OopMap,只會在Safe Point處記錄這些信息恋谭。
所以Safe Point的選擇很重要俗扇,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致運行時的性能問題箕别。大部分指令的執(zhí)行時間都非常短暫铜幽,通常會選擇一些執(zhí)行時間較長的指令作為Safe Point,如方法調(diào)用串稀、循環(huán)跳轉和異常跳轉等除抛。
關于Safe Point更多的信息,可以看看這篇文章 JVM的Stop The World母截,安全點到忽,黑暗的地底世界
發(fā)生GC時,如何讓所有線程跑到最近的Safe Point再暫停清寇?
當發(fā)生GC時喘漏,不直接對線程進行中斷操作,而是簡單的設置一個中斷標志华烟,每個線程運行到Safe Point的時候翩迈,主動去輪詢這個中斷標志,如果中斷標志為真盔夜,則將自己進行中斷掛起负饲。
這里忽略了一個問題堤魁,當發(fā)生GC時,運行中的線程可以跑到Safe Point后進行掛起返十,而那些處于Sleep或Blocked狀態(tài)的線程在此時無法響應JVM的中斷請求妥泉,無法到Safe Point處進行掛起,針對這種情況洞坑,可以使用安全區(qū)域(Safe Region)進行解決盲链。
Safe Region是指在一段代碼片段中,對象的引用關系不會發(fā)生變化迟杂,在這個區(qū)域中的任何位置開始GC都是安全的刽沾。
1、當線程運行到Safe Region的代碼時逢慌,首先標識已經(jīng)進入了Safe Region悠轩,如果這段時間內(nèi)發(fā)生GC间狂,JVM會忽略標識為Safe Region狀態(tài)的線程攻泼;
2、當線程即將離開Safe Region時鉴象,會檢查JVM是否已經(jīng)完成GC忙菠,如果完成了,則繼續(xù)運行纺弊,否則線程必須等待直到收到可以安全離開Safe Region的信號為止牛欢;
垃圾收集器
Java虛擬機規(guī)范并沒有規(guī)定垃圾收集器應該如何實現(xiàn),用戶可以根據(jù)系統(tǒng)特點對各個區(qū)域所使用的收集器進行組合使用淆游。
上圖展示了7種不同分代的收集器傍睹,如果兩兩之間存在連線,說明可以組合使用犹菱。
1拾稳、Serial收集器(串行GC)
Serial 是一個采用單個線程并基于復制算法工作在新生代的收集器,進行垃圾收集時腊脱,必須暫停其他所有的工作線程访得。對于單CPU環(huán)境來說,Serial由于沒有線程交互的開銷陕凹,可以很高效的進行垃圾收集動作悍抑,是Client模式下新生代默認的收集器。
2杜耙、ParNew收集器(并行GC)
ParNew其實是serial的多線程版本搜骡,除了使用多條線程進行垃圾收集之外,其余行為與Serial一樣佑女。
3浆兰、Parallel Scavenge收集器(并行回收GC)
Parallel Scavenge是一個采用多線程基于復制算法并工作在新生代的收集器磕仅,其關注點在于達到一個可控的吞吐量,經(jīng)常被稱為“吞吐量優(yōu)先”的收集器簸呈。
吞吐量 = 用戶代碼運行時間 /(用戶代碼運行時間 + 垃圾收集時間)
Parallel Scavenge提供了兩個參數(shù)用于精確控制吞吐量:
1榕订、-XX:MaxGCPauseMillis 設置垃圾收集的最大停頓時間
2、-XX:GCTimeRatio 設置吞吐量大小
4蜕便、Serial Old收集器(串行GC)
Serial Old 是一個采用單線程基于標記-整理算法并工作在老年代的收集器劫恒,是Client模式下老年代默認的收集器。
5轿腺、Parallel Old收集器(并行GC)
Parallel Old是一個采用多線程基于標記-整理算法并工作在老年代的收集器两嘴。在注重吞吐量以及CPU資源敏感的場合,可以優(yōu)先考慮Parallel Scavenge和Parallel Old的收集器組合族壳。
6憔辫、CMS收集器(并發(fā)GC)
CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間為目標的收集器,工作在老年代仿荆,基于“標記-清除”算法實現(xiàn)贰您,整個過程分為以下4步:
1、初始標記:這個過程只是標記以下GC Roots能夠直接關聯(lián)的對象拢操,但是仍然會Stop The World锦亦;
2、并發(fā)標記:進行GC Roots Tracing的過程令境,可以和用戶線程一起工作杠园。
3、重新標記:用于修正并發(fā)標記期間由于用戶程序繼續(xù)運行而導致標記產(chǎn)生變動的那部分記錄舔庶,這個過程會暫停所有線程抛蚁,但其停頓時間遠比并發(fā)標記的時間短;
4惕橙、并發(fā)清理:可以和用戶線程一起工作瞧甩。
CMS收集器的缺點:
1、對CPU資源比較敏感吕漂,在并發(fā)階段亲配,雖然不會導致用戶線程停頓,但是會占用一部分線程資源惶凝,降低系統(tǒng)的總吞吐量吼虎。
2、無法處理浮動垃圾苍鲜,在并發(fā)清理階段思灰,用戶線程的運行依然會產(chǎn)生新的垃圾對象,這部分垃圾只能在下一次GC時收集混滔。
3洒疚、CMS是基于標記-清除算法實現(xiàn)的歹颓,意味著收集結束后會造成大量的內(nèi)存碎片,可能導致出現(xiàn)老年代剩余空間很大油湖,卻無法找到足夠大的連續(xù)空間分配當前對象巍扛,不得不提前觸發(fā)一次Full GC。
JDK1.5實現(xiàn)中乏德,當老年代空間使用率達到68%時撤奸,就會觸發(fā)CMS收集器,如果應用中老年代增長不是太快喊括,可以通過-XX:CMSInitiatingOccupancyFraction參數(shù)提高觸發(fā)百分比胧瓜,從而降低內(nèi)存回收次數(shù)提高系統(tǒng)性能。
JDK1.6實現(xiàn)中郑什,觸發(fā)CMS收集器的閾值已經(jīng)提升到92%府喳,要是CMS運行期間預留的內(nèi)存無法滿足用戶線程需要,會出現(xiàn)一次”Concurrent Mode Failure”失敗蘑拯,這是虛擬機會啟動Serial Old收集器對老年代進行垃圾收集钝满,當然,這樣應用的停頓時間就更長了强胰,所以這個閾值也不能設置的太高舱沧,如果導致了”Concurrent Mode Failure”失敗妹沙,反而會降低性能偶洋,至于如何設置這個閾值,還得長時間的對老年代空間的使用情況進行監(jiān)控距糖。
7玄窝、G1收集器
G1(Garbage First)是JDK1.7提供的一個工作在新生代和老年代的收集器,基于“標記-整理”算法實現(xiàn)悍引,在收集結束后可以避免內(nèi)存碎片問題恩脂。
G1優(yōu)點:
1、并行與并發(fā):充分利用多CPU來縮短Stop The World的停頓時間趣斤;
2俩块、分代收集:不需要其他收集配合就可以管理整個Java堆,采用不同的方式處理新建的對象浓领、已經(jīng)存活一段時間和經(jīng)歷過多次GC的對象獲取更好的收集效果;
3玉凯、空間整合:與CMS的”標記-清除”算法不同,G1在運行期間不會產(chǎn)生內(nèi)存空間碎片联贩,有利于應用的長時間運行漫仆,且分配大對象時,不會導致由于無法申請到足夠大的連續(xù)內(nèi)存而提前觸發(fā)一次Full GC;
4泪幌、停頓預測:G1中可以建立可預測的停頓時間模型盲厌,能讓使用者明確指定在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堆上任意的對象有引用關系啊央,那么如何判定一個對象是否存活,是否需要掃描整個Java堆涨醋?其實這個問題在之前收集器中也存在瓜饥,如果回收新生代的對象時,不得不同時掃描老年代的話浴骂,會大大降低Minor GC的效率乓土。
針對這種情況,虛擬機提供了一個解決方案:G1收集器中Region之間的對象引用關系和其他收集器中新生代與老年代之間的對象引用關系被保存在Remenbered Set數(shù)據(jù)結構中溯警,用來避免全堆掃描趣苏。G1中每個Region都有一個對應的Remenbered Set,當虛擬機發(fā)現(xiàn)程序對Reference類型的數(shù)據(jù)進行寫操作時梯轻,會產(chǎn)生一個Write Barrier暫時中斷寫操作食磕,檢查Reference引用的對象是否處于相同的Region中,如果不是喳挑,則通過CardTable把相關引用信息記錄到被引用對象所屬Region的Remenbered Set中彬伦。
在互聯(lián)網(wǎng)公司面試中,架構的底層一定是面試官會問到的問題伊诵,針對面試官一般會提到的問題单绑,我錄制了一些分布式,微服務日戈,性能優(yōu)化等技術點底層原理的錄像視頻询张,加群619881427可以免費獲取這些錄像,里面還有些分布式浙炼,微服務份氧,性能優(yōu)化唯袄,Spring,MyBatis的等源碼知識點的錄像視頻蜗帜。這些視頻都是我找一些資深架構師朋友一起錄制出來的恋拷,這些視頻幫助以下幾類程序員:
1.對現(xiàn)在的薪資不滿,想要跳槽厅缺,卻對自己的技術沒有信心蔬顾,不知道如何面對面試官。
2.想從傳統(tǒng)行業(yè)轉行到互聯(lián)網(wǎng)行業(yè)湘捎,但沒有接觸過互聯(lián)網(wǎng)技術诀豁。
3.工作1 - 5年需要提升自己的核心競爭力,但學習沒有系統(tǒng)化窥妇,不知道自己接下來要學什么才是正確的舷胜,踩坑后又不知道找誰,百度后依然不知所以然活翩。
4.工作5 - 10年無法突破技術瓶頸(運用過很多技術烹骨,在公司一直寫著業(yè)務代碼,卻依然不懂底層實現(xiàn)原理)
如果你現(xiàn)在正處于我上述所說的幾個階段可以加下我的群來學習材泄。而且我也能夠提供一些面試指導沮焕,職業(yè)規(guī)劃等建議。