前言
JAVA GC(Garbage Collection,垃圾回收)機(jī)制是區(qū)別C++的一個(gè)重要特征,C++需要開(kāi)發(fā)者自己實(shí)現(xiàn)垃圾回收的邏輯,而JAVA開(kāi)發(fā)者則只需要專注于業(yè)務(wù)開(kāi)發(fā),因?yàn)槔厥者@件繁瑣的事情JVM已經(jīng)為我們代勞了,從這一點(diǎn)上來(lái)說(shuō),JAVA還是要做的比較完善一些。但這并不意味著我們不用去理解GC機(jī)制的原理,因?yàn)槿绻涣私馄湓?,可能?huì)引發(fā)內(nèi)存泄漏、頻繁GC導(dǎo)致應(yīng)用卡頓,甚至出現(xiàn)OOM等問(wèn)題,因此我們需要深入理解其原理,才能編寫出高性能的應(yīng)用程序,解決性能瓶頸。
想要理解GC的原理,我們必須先理解JVM內(nèi)存管理機(jī)制,因?yàn)檫@樣我們才能知道回收哪些對(duì)象、什么時(shí)候回收以及怎么回收。
JVM內(nèi)存管理
根據(jù)JVM規(guī)范,JVM把內(nèi)存劃分成了如下幾個(gè)區(qū)域:
- 方法區(qū)(Method Area)
- 堆區(qū)(Heap)
- 虛擬機(jī)棧(VM Stack)
- 本地方法棧(Native Method Stack)
- 程序計(jì)數(shù)器(Program Counter Register)
其中,方法區(qū)和堆所有線程共享。
方法區(qū)(Method Area)
方法區(qū)存放了要加載的類的信息(如類名、修飾符等)、靜態(tài)變量、構(gòu)造函數(shù)、final定義的常量、類中的字段和方法等信息。方法區(qū)是全局共享的,在一定條件下也會(huì)被GC。當(dāng)方法區(qū)超過(guò)它允許的大小時(shí),就會(huì)拋出OutOfMemory:PermGen Space異常。
在Hotspot虛擬機(jī)中,這塊區(qū)域?qū)?yīng)持久代(Permanent Generation),一般來(lái)說(shuō),方法區(qū)上執(zhí)行GC的情況很少,因此方法區(qū)被稱為持久代的原因之一,但這并不代表方法區(qū)上完全沒(méi)有GC,其上的GC主要針對(duì)常量池的回收和已加載類的卸載。在方法區(qū)上進(jìn)行GC,條件相當(dāng)苛刻而且困難。
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分,用于存儲(chǔ)編譯器生成的常量和引用。一般來(lái)說(shuō),常量的分配在編譯時(shí)就能確定,但也不全是,也可以存儲(chǔ)在運(yùn)行時(shí)期產(chǎn)生的常量。比如String類的intern()方法,作用是String類維護(hù)了一個(gè)常量池,如果調(diào)用的字符”hello”已經(jīng)在常量池中,則直接返回常量池中的地址,否則新建一個(gè)常量加入池中,并返回地址。
堆區(qū)(Heap)
堆區(qū)是GC最頻繁的,也是理解GC機(jī)制最重要的區(qū)域。堆區(qū)由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆區(qū)主要用于存放對(duì)象實(shí)例及數(shù)組,所有new出來(lái)的對(duì)象都存儲(chǔ)在該區(qū)域。
虛擬機(jī)棧(VM Stack)
虛擬機(jī)棧占用的是操作系統(tǒng)內(nèi)存,每個(gè)線程對(duì)應(yīng)一個(gè)虛擬機(jī)棧,它是線程私有的,生命周期和線程一樣,每個(gè)方法被執(zhí)行時(shí)產(chǎn)生一個(gè)棧幀(Statck Frame),棧幀用于存儲(chǔ)局部變量表、動(dòng)態(tài)鏈接、操作數(shù)和方法出口等信息,當(dāng)方法被調(diào)用時(shí),棧幀入棧,當(dāng)方法調(diào)用結(jié)束時(shí),棧幀出棧。
局部變量表中存儲(chǔ)著方法相關(guān)的局部變量,包括各種基本數(shù)據(jù)類型及對(duì)象的引用地址等,因此他有個(gè)特點(diǎn):內(nèi)存空間可以在編譯期間就確定,運(yùn)行時(shí)不再改變。
虛擬機(jī)棧定義了兩種異常類型:StackOverFlowError(棧溢出)和OutOfMemoryError(內(nèi)存溢出)。如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度,則拋出StackOverFlowError;不過(guò)大多數(shù)虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小,所以線程可以一直申請(qǐng)棧,直到內(nèi)存不足時(shí),拋出OutOfMemoryError。
本地方法棧(Native Method Stack)
本地方法棧用于支持native方法的執(zhí)行,存儲(chǔ)了每個(gè)native方法的執(zhí)行狀態(tài)。本地方法棧和虛擬機(jī)棧他們的運(yùn)行機(jī)制一致,唯一的區(qū)別是,虛擬機(jī)棧執(zhí)行Java方法,本地方法棧執(zhí)行native方法。在很多虛擬機(jī)中(如Sun的JDK默認(rèn)的HotSpot虛擬機(jī)),會(huì)將虛擬機(jī)棧和本地方法棧一起使用。
程序計(jì)數(shù)器(Program Counter Register)
程序計(jì)數(shù)器是一個(gè)很小的內(nèi)存區(qū)域,不在RAM上,而是直接劃分在CPU上,程序猿無(wú)法操作它,它的作用是:JVM在解釋字節(jié)碼(.class)文件時(shí),存儲(chǔ)當(dāng)前線程執(zhí)行的字節(jié)碼行號(hào),只是一種概念模型,各種JVM所采用的方式不一樣。字節(jié)碼解釋器工作時(shí),就是通過(guò)改變程序計(jì)數(shù)器的值來(lái)取下一條要執(zhí)行的指令,分支、循環(huán)、跳轉(zhuǎn)等基礎(chǔ)功能都是依賴此技術(shù)區(qū)完成的。
每個(gè)程序計(jì)數(shù)器只能記錄一個(gè)線程的行號(hào),因此它是線程私有的。
如果程序當(dāng)前正在執(zhí)行的是一個(gè)java方法,則程序計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址,如果執(zhí)行的是native方法,則計(jì)數(shù)器的值為空,此內(nèi)存區(qū)是唯一不會(huì)拋出OutOfMemoryError的區(qū)域。
GC機(jī)制
隨著程序的運(yùn)行,內(nèi)存中的實(shí)例對(duì)象、變量等占據(jù)的內(nèi)存越來(lái)越多,如果不及時(shí)進(jìn)行回收,會(huì)降低程序運(yùn)行效率,甚至引發(fā)系統(tǒng)異常。
在上面介紹的五個(gè)內(nèi)存區(qū)域中,有3個(gè)是不需要進(jìn)行垃圾回收的:本地方法棧、程序計(jì)數(shù)器、虛擬機(jī)棧。因?yàn)樗麄兊纳芷谑呛途€程同步的,隨著線程的銷毀,他們占用的內(nèi)存會(huì)自動(dòng)釋放。所以,只有方法區(qū)和堆區(qū)需要進(jìn)行垃圾回收,回收的對(duì)象就是那些不存在任何引用的對(duì)象。
查找算法
- 引用計(jì)數(shù)法
- 可達(dá)性分析
引用計(jì)數(shù)法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器加一。反之每當(dāng)一個(gè)引用失效時(shí),計(jì)數(shù)器減一。當(dāng)計(jì)數(shù)器為0時(shí),則表示對(duì)象不被引用,即認(rèn)為該對(duì)象可以被回收了。但是這個(gè)算法有個(gè)明顯的缺陷:當(dāng)兩個(gè)對(duì)象相互引用,但是二者都已經(jīng)沒(méi)有作用時(shí),理應(yīng)把它們都回收,但是由于它們相互引用,不符合垃圾回收的條件,所以就導(dǎo)致無(wú)法處理掉這一塊內(nèi)存區(qū)域。
可達(dá)性分析
設(shè)立若干根對(duì)象(GC Root),每個(gè)對(duì)象都是一個(gè)子節(jié)點(diǎn),當(dāng)一個(gè)對(duì)象找不到根時(shí),就認(rèn)為該對(duì)象不可達(dá)。
java中,可以作為GC Roots的對(duì)象包括:
- java虛擬機(jī)棧中引用的對(duì)象
- 方法區(qū)中靜態(tài)變量引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中引用的對(duì)象
補(bǔ)充概念,在JDK1.2之后引入了四個(gè)概念:強(qiáng)引用、軟引用、弱引用、虛引用。
- 強(qiáng)引用:new出來(lái)的對(duì)象都是強(qiáng)引用,GC無(wú)論如何都不會(huì)回收,即使拋出OOM異常。
- 軟引用:只有當(dāng)JVM內(nèi)存不足時(shí)才會(huì)被回收。
- 弱引用:只要GC,就會(huì)立馬回收,不管內(nèi)存是否充足。
- 虛引用:可以忽略不計(jì),JVM完全不會(huì)在乎虛引用,你可以理解為它是來(lái)湊數(shù)的,湊夠”四大天王”。它唯一的作用就是做一些跟蹤記錄,輔助finalize函數(shù)的使用。
最后總結(jié),什么樣的類需要被回收:
a.該類的所有實(shí)例都已經(jīng)被回收;
b.加載該類的ClassLoad已經(jīng)被回收;
c.該類對(duì)應(yīng)的反射類java.lang.Class對(duì)象沒(méi)有被任何地方引用。
內(nèi)存分區(qū)
內(nèi)存主要被分為三塊:新生代(Youn Generation)、舊生代(Old Generation)、持久代(Permanent Generation)。三代的特點(diǎn)不同,造就了他們使用的GC算法不同,新生代適合生命周期較短,快速創(chuàng)建和銷毀的對(duì)象,舊生代適合生命周期較長(zhǎng)的對(duì)象,持久代在Sun Hotpot虛擬機(jī)中就是指方法區(qū)(有些JVM根本就沒(méi)有持久代這一說(shuō)法)。
新生代(Youn Generation):大致分為Eden區(qū)和Survivor區(qū),Survivor區(qū)又分為大小相同的兩部分:FromSpace和ToSpace。新建的對(duì)象都是從新生代分配內(nèi)存,Eden區(qū)不足的時(shí)候,會(huì)把存活的對(duì)象轉(zhuǎn)移到Survivor區(qū)。當(dāng)新生代進(jìn)行垃圾回收時(shí)會(huì)觸發(fā)Minor GC(也稱作Youn GC)。
舊生代(Old Generation):舊生代用于存放新生代多次回收依然存活的對(duì)象,如緩存對(duì)象。當(dāng)舊生代滿了的時(shí)候就需要對(duì)舊生代進(jìn)行回收,舊生代的垃圾回收稱作Major GC(也稱作Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是方法區(qū)的意思,盡管大多數(shù)JVM沒(méi)有這一代。
GC算法
常見(jiàn)的GC算法:復(fù)制、標(biāo)記-清除和標(biāo)記-壓縮
復(fù)制:
復(fù)制算法采用的方式為從根集合進(jìn)行掃描,將存活的對(duì)象移動(dòng)到一塊空閑的區(qū)域。
當(dāng)存活的對(duì)象較少時(shí),復(fù)制算法會(huì)比較高效(新生代的Eden區(qū)就是采用這種算法),其帶來(lái)的成本是需要一塊額外的空閑空間和對(duì)象的移動(dòng)。
標(biāo)記-清除:
該算法采用的方式是從跟集合開(kāi)始掃描,對(duì)存活的對(duì)象進(jìn)行標(biāo)記,標(biāo)記完畢后,再掃描整個(gè)空間中未被標(biāo)記的對(duì)象,并進(jìn)行清除。在Marking階段,需要進(jìn)行全盤掃描,這個(gè)過(guò)程是比較耗時(shí)的。清除階段清理的是沒(méi)有被引用的對(duì)象,存活的對(duì)象被保留。
標(biāo)記-清除動(dòng)作不需要移動(dòng)對(duì)象,且僅對(duì)不存活的對(duì)象進(jìn)行清理,在空間中存活對(duì)象較多的時(shí)候,效率較高,但由于只是清除,沒(méi)有重新整理,因此會(huì)造成內(nèi)存碎片。
標(biāo)記-壓縮:
該算法與標(biāo)記-清除算法類似,都是先對(duì)存活的對(duì)象進(jìn)行標(biāo)記,但是在清除后會(huì)把活的對(duì)象向左端空閑空間移動(dòng),然后再更新其引用對(duì)象的指針。
由于進(jìn)行了移動(dòng)規(guī)整動(dòng)作,該算法避免了標(biāo)記-清除的碎片問(wèn)題,但由于需要進(jìn)行移動(dòng),因此成本也增加了。(該算法適用于舊生代)
垃圾收集器
在JVM中,GC是由垃圾回收器來(lái)執(zhí)行,所以,在實(shí)際應(yīng)用場(chǎng)景中,我們需要選擇合適的垃圾收集器,下面我們介紹一下垃圾收集器。
串行收集器(Serial GC)
Serial GC是最古老也是最基本的收集器,但是現(xiàn)在依然廣泛使用,JAVA SE5和JAVA SE6中客戶端虛擬機(jī)采用的默認(rèn)配置。比較適合于只有一個(gè)處理器的系統(tǒng)。在串行處理器中minor和major GC過(guò)程都是用一個(gè)線程進(jìn)行回收的。它的最大特點(diǎn)是在進(jìn)行垃圾回收時(shí),需要對(duì)所有正在執(zhí)行的線程暫停(stop the world),對(duì)于有些應(yīng)用是難以接受的,但是如果應(yīng)用的實(shí)時(shí)性要求不是那么高,只要停頓的時(shí)間控制在N毫秒之內(nèi),大多數(shù)應(yīng)用還是可以接受的,而且事實(shí)上,它并沒(méi)有讓我們失望,幾十毫秒的停頓,對(duì)于我們客戶機(jī)是完全可以接受的,該收集器適用于單CPU、新生代空間較小且對(duì)暫停時(shí)間要求不是特別高的應(yīng)用上,是client級(jí)別的默認(rèn)GC方式。
ParNew GC
基本和Serial GC一樣,但本質(zhì)區(qū)別是加入了多線程機(jī)制,提高了效率,這樣它就可以被用于服務(wù)端上(server),同時(shí)它可以與CMS GC配合,所以,更加有理由將他用于server端。
Parallel Scavenge GC
在整個(gè)掃描和復(fù)制過(guò)程采用多線程的方式進(jìn)行,適用于多CPU、對(duì)暫停時(shí)間要求較短的應(yīng)用,是server級(jí)別的默認(rèn)GC方式。
CMS (Concurrent Mark Sweep)收集器
該收集器的目標(biāo)是解決Serial GC停頓的問(wèn)題,以達(dá)到最短回收時(shí)間。常見(jiàn)的B/S架構(gòu)的應(yīng)用就適合這種收集器,因?yàn)槠涓卟l(fā)、高響應(yīng)的特點(diǎn),CMS是基于標(biāo)記-清楚算法實(shí)現(xiàn)的。
CMS收集器的優(yōu)點(diǎn):并發(fā)收集、低停頓,但遠(yuǎn)沒(méi)有達(dá)到完美;
CMS收集器的缺點(diǎn):
a. CMS收集器對(duì)CPU資源非常敏感,在并發(fā)階段雖然不會(huì)導(dǎo)致用戶停頓,但是會(huì)占用CPU資源而導(dǎo)致應(yīng)用程序變慢,總吞吐量下降。
b. CMS收集器無(wú)法處理浮動(dòng)垃圾,可能出現(xiàn)“Concurrnet Mode Failure”,失敗而導(dǎo)致另一次的Full GC。
c. CMS收集器是基于標(biāo)記-清除算法的實(shí)現(xiàn),因此也會(huì)產(chǎn)生碎片。
G1收集器
相比CMS收集器有不少改進(jìn),首先,基于標(biāo)記-壓縮算法,不會(huì)產(chǎn)生內(nèi)存碎片,其次可以比較精確的控制停頓。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣使用一個(gè)單線程執(zhí)行收集,使用“標(biāo)記-整理”算法。主要使用在Client模式下的虛擬機(jī)。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。
RTSJ垃圾收集器
RTSJ垃圾收集器,用于Java實(shí)時(shí)編程。
總結(jié)
深入理解JVM的內(nèi)存模型和GC機(jī)制有助于幫助我們編寫高性能代碼和提供代碼優(yōu)化的思路與方向。