以下信息摘錄自:深入理解JVM的內(nèi)存結(jié)構(gòu)及GC機(jī)制
JVM內(nèi)存管理
JVM虛擬機(jī)常見(jiàn)面試題
https://mp.weixin.qq.com/s/eULjdiqj0RnWerruzWnDJA
根據(jù)JVM規(guī)范,JVM把內(nèi)存區(qū)域劃分成了以下幾個(gè)區(qū)域:
1.方法區(qū)(Method Area)
2.堆區(qū)(Heap)
3.虛擬機(jī)棧(VM Stack)
4.本地方法棧(Native Method Stack)
5.程序計(jì)數(shù)器(Program Counter Register)

其中方法區(qū)和堆是所有線程共享的
方法區(qū)(Method Area)
方法區(qū)存放的是要加載的類的信息(包括類型、修飾符等)、靜態(tài)變量(關(guān)于靜態(tài)變量和靜態(tài)方法的存儲(chǔ)請(qǐng)參考:# where is a static method and a static variable stored in java. In heap or in stack memory)、構(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的可能,方法區(qū)中的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發(fā)生最頻繁的地方,也是理解GC機(jī)制最重要的區(qū)域。堆區(qū)是由所有的線程共享的,在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建。堆區(qū)主要用于存放對(duì)象實(shí)例及數(shù)組,凡是通過(guò)new生成的對(duì)象都存放在堆中,對(duì)于堆中的對(duì)象生命周期的管理由Java虛擬機(jī)的垃圾回收機(jī)制GC進(jìn)行回收和統(tǒng)一管理。類的非靜態(tài)成員變量也放在堆區(qū),其中基本數(shù)據(jù)類型是直接保存值,而復(fù)雜類型是保存指向?qū)ο蟮囊茫庆o態(tài)成員變量在類的實(shí)例化時(shí)開(kāi)辟空間并且初始化。所以你要知道類的幾個(gè)時(shí)機(jī),加載-連接-初始化-實(shí)例化。
虛擬機(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)存空間可以在編譯時(shí)間就確定,運(yùn)行時(shí)不再改變。
虛擬機(jī)棧定義了兩種異常類型:StackOverFlowError(棧溢出)和OutOfMemoryError(內(nèi)存溢出)。如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度(比如遞歸調(diào)用),則會(huì)拋出StackOverFlowError,不過(guò)大多數(shù)虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小,所以線程可以一直申請(qǐng)棧內(nèi)存,知道內(nèi)存不足時(shí),拋出OutOfMemoryError。
本地方法棧(Native Method Stack)
本地方法棧用于支持native方法的執(zhí)行,存儲(chǔ)了每個(gè)native方法的執(zhí)行狀態(tài)。本地方法棧和虛擬機(jī)棧他們的運(yùn)行的方式一致,唯一得到區(qū)別就是:虛擬機(jī)棧執(zhí)行jaba方法,本地方法棧執(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ū)來(lái)完成的。
每個(gè)程序計(jì)數(shù)器只能記錄一個(gè)線程的行號(hào),因此它是線程私有的。
如果程序當(dāng)前正在執(zhí)行的是一個(gè)Java方法,則程序計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)指令的地址,如果執(zhí)行的是native方法,則計(jì)數(shù)器的值為空,此內(nèi)存區(qū)是唯一不會(huì)拋出OutMemoryError的區(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)行內(nèi)存回收的:本地方法棧、程序計(jì)數(shù)器、虛擬機(jī)棧。因?yàn)樗麄兊纳芷谑呛途€程同步的,隨著線程的銷毀,他們占用的內(nèi)存會(huì)自動(dòng)釋放。所以,只有方法區(qū)和堆區(qū)需要進(jìn)行垃圾回收,回收的對(duì)象就是那些不存在任何引用的對(duì)象。
查找算法
經(jīng)典的引用計(jì)數(shù)算法,每個(gè)對(duì)象添加到引用計(jì)數(shù)器,每被引用一次,計(jì)數(shù)器+1,失去瑩瑩,計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器在一段時(shí)間內(nèi)為0時(shí),即認(rèn)為該對(duì)象可以被回收了。但是這個(gè)算法有一個(gè)明顯的缺陷,當(dāng)存在兩個(gè)對(duì)象相互引用時(shí),但這兩個(gè)對(duì)象都沒(méi)被其他對(duì)象引用時(shí),理論上應(yīng)該把二者都回收掉,但是由于他們自己相互引用造成計(jì)數(shù)器不為0,不符合垃圾回收的條件,所以就導(dǎo)致無(wú)法回收這一塊內(nèi)存區(qū)域。因此,sun的JVM并沒(méi)有采用這種算法,而是采用一個(gè)叫做根搜索算法,如圖:

基本思想是:從一個(gè)叫做GC的根節(jié)點(diǎn)出發(fā),向下搜索,如果一個(gè)對(duì)象不能達(dá)到GC ROOTS的時(shí)候,說(shuō)明該對(duì)象不再被引用,可以被回收。如上圖中Object5、Object6、Object7,雖然他們?nèi)齻€(gè)依賴相互引用,但是他們其實(shí)已經(jīng)沒(méi)有作用了,這樣就解決了引用計(jì)數(shù)算法的缺陷。
補(bǔ)充概念,在JDK1.2之后引入了四個(gè)概念:強(qiáng)引用、軟引用、弱引用、虛引用。
強(qiáng)引用:只要是直接new出來(lái)的對(duì)象都是強(qiáng)引用,當(dāng)對(duì)象還被其他變量引用的時(shí)候GC無(wú)論如何都不會(huì)回收,即使拋出OOM異常。
軟引用:只有當(dāng)JVM內(nèi)存不足時(shí)才會(huì)被回收。
弱引用:只要GC,就會(huì)立馬被回收,不管內(nèi)存時(shí)候充足。
虛引用:可以忽略不計(jì),JVM完全不在乎虛引用,你可以理解它是用來(lái)湊數(shù)的。它唯一的作用就是做一些跟蹤記錄,輔助finalize函數(shù)的使用。
最后總結(jié)一下,什么樣的對(duì)象需要被回收:
1.該類的所有實(shí)例都已經(jīng)被回收;
2.加載該類的ClassLoad已經(jīng)被回收;
3.該類對(duì)應(yīng)的反射類java.lang.Class對(duì)象沒(méi)有被任何實(shí)例引用。
內(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):舊生代用于存儲(chǔ)新生代多次回收依然存活的對(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)行清除。標(biāo)記-清除的過(guò)程如下:

上圖中藍(lán)色部分是有被引用的對(duì)象,黃色部分是沒(méi)有被引用的對(duì)象。在標(biāo)記階段,需要進(jìn)行全盤(pán)掃描,這個(gè)過(guò)程是比較耗時(shí)的。

清除階段清理的是沒(méi)有被引用的對(duì)象,存活的對(duì)象唄保留。
標(biāo)記-清除動(dòng)作不需要移動(dòng)對(duì)象,且僅對(duì)不存活的對(duì)象進(jìn)行清理,在空間中存活對(duì)象較多時(shí)比較高效,但由于只是清除,沒(méi)有重新整理,當(dāng)未被引用對(duì)象較多時(shí),會(huì)造成內(nèi)存碎片。
標(biāo)記-壓縮:該算法與標(biāo)記-清除算法類似,都是想對(duì)存活的對(duì)象進(jìn)行標(biāo)記,但是在清除后會(huì)把活的對(duì)象向左端空間空間進(jìn)行移動(dòng),然后再更新其引用對(duì)象的指針,如下圖所示:

由于進(jìn)行了移動(dòng)規(guī)整動(dòng)作,改算法避免了標(biāo)記-清除算法的內(nèi)存碎片問(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èi),大多數(shù)應(yīng)用還是可以接受的,而且,事實(shí)上它并沒(méi)有讓我們失望,幾十毫秒的停頓,對(duì)于我們客戶端是完全可以接受的,該收集器適用于單CPU、新生代控件較小且對(duì)暫停時(shí)間要求不是特別高的應(yīng)用上,是client級(jí)別的默認(rèn)GC方式。
ParNew GC(新式GC)
基本上和serial GC一樣,單本質(zhì)卻別就是加入了多線程機(jī)制,提高了效率,這樣它就可以被用于服務(wù)端上,同時(shí)它可以與CMS GC配合,所以,更加有理由將他用于server端。
Parallel Scavenge GC(并行清除GC)
在整個(gè)掃描和復(fù)制過(guò)程采用多線程的方式進(jìn)行,適用于多CPU、對(duì)暫停時(shí)間要求較短的應(yīng)用,是server級(jí)別的默認(rèn)GC方式。
CMS (Concurrent Mark Sweep)GC
該收集器的目標(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):
1.CMS收集器對(duì)CPU資源非常敏感,在并發(fā)階段雖然不會(huì)導(dǎo)致用戶停頓,但是會(huì)占用CPU資源而導(dǎo)致應(yīng)用程序變慢,總吞吐量下降。
2.CMS收集器無(wú)法處理浮動(dòng)垃圾,可能出現(xiàn)“Concurrnet Mode Failure”,失敗而導(dǎo)致另一次的Full GC。
3.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í)編程