JVM和GC
JVM運(yùn)行時(shí)內(nèi)存區(qū)

一、線程私有數(shù)據(jù)區(qū)
1、程序計(jì)數(shù)器
在JVM中,多線程是通過(guò)線程輪流切換來(lái)獲得CPU執(zhí)行時(shí)間的,因此,在任一具體時(shí)刻,一個(gè)CPU的內(nèi)核只會(huì)執(zhí)行一條線程中的指令,因此為了能夠使得每個(gè)線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置,每個(gè)線程都需要有自己獨(dú)立的程序計(jì)數(shù)器,并且不能相互干擾,否則就會(huì)影響到程序的正確執(zhí)行次序。程序計(jì)數(shù)器中記錄的是正在執(zhí)行的線程的虛擬機(jī)字節(jié)碼指令的地址,字節(jié)碼的解釋器工作的時(shí)候就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令。程序計(jì)數(shù)器是每個(gè)線程私有的。
2、虛擬機(jī)棧
虛擬機(jī)棧也就是我們常說(shuō)的棧。虛擬機(jī)棧是Java方法執(zhí)行的內(nèi)存模型。Java棧中存放的是一個(gè)個(gè)棧幀。并且是線程私有的,生命周期與線程相同,描述的是Java方法執(zhí)行的內(nèi)存模型:每一個(gè)方法執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法的執(zhí)行就是對(duì)應(yīng)棧幀在虛擬機(jī)棧中的入棧、出棧的過(guò)程。下圖表示了一個(gè)Java棧的模型:

3、本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用很相似,他們的區(qū)別在于虛擬機(jī)棧為執(zhí)行Java代碼方法服務(wù),而本地方法棧為Native方法服務(wù)。
二、線程共享區(qū)域
1、Java堆
Java堆可以說(shuō)是虛擬機(jī)中最大的一塊內(nèi)存了。它是所有線程共享的內(nèi)存區(qū)域,幾乎所有的實(shí)例對(duì)象都是在這塊區(qū)域中存放。堆可以處理物理上不連續(xù)的內(nèi)存空間,只要邏輯上連續(xù)的就可以。當(dāng)然,隨著JIT(just in time,及時(shí)編譯技術(shù)) 編譯器的發(fā)展,所有對(duì)象在"堆"上分配也變得不那么"絕對(duì)"了。同時(shí)Java堆也是垃圾收集器管理的主要區(qū)域。由于現(xiàn)在收集器基本上采用的都是分代收集算法,所有Java堆又可以細(xì)分為:"新生代"和"老年代"。再細(xì)致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。
2、方法區(qū)
方法區(qū)在JVM中也是一個(gè)非常重要的區(qū)域,在方法區(qū)中,存儲(chǔ)了每個(gè)類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。它與堆一樣,是被線程共享的區(qū)域,很容易理解,我們?cè)趯慗ava代碼時(shí),每個(gè)線程都可以訪問(wèn)同一個(gè)類的靜態(tài)變量。在Class文件中除了類的字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用來(lái)存儲(chǔ)編譯期間生成的字面量和符號(hào)引用。
垃圾回收
哪些對(duì)象需要回收
1、引用計(jì)數(shù)法:判斷對(duì)象的引用數(shù)量
引用計(jì)數(shù)法是通過(guò)判斷對(duì)象的引用數(shù)量來(lái)決定對(duì)象是否可以被回收
給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用他時(shí),計(jì)數(shù)器值就+1,;當(dāng)引用失效時(shí),計(jì)數(shù)器值就-1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能在被使用。
- 優(yōu)點(diǎn):
判定效率很高
- 確定:
不會(huì)完全準(zhǔn)確,因?yàn)槿绻霈F(xiàn)兩個(gè)對(duì)象相互引用的問(wèn)題就不行了,如下圖所示:

如上圖對(duì)象A和對(duì)象B相互引用,導(dǎo)致他們的引用計(jì)數(shù)都不為0,那么垃圾收集器就永遠(yuǎn)不會(huì)回收他們。
2、可達(dá)性分析算法:判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá)
通過(guò)一系列的GC Roots的對(duì)象作為起始點(diǎn),從這些根節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。

上圖中,ObjD和ObjE都是不可用的,可以被GC回收掉。
在Java中,可作為 GC Root 的對(duì)象包括以下幾種:
- 虛擬機(jī)棧(棧幀中的局部變量表)中引用的對(duì)象
- 方法區(qū)中靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中Native引用的對(duì)象
垃圾收集算法
1、標(biāo)記清除算法
標(biāo)記清除即Mark-Sweep,是一種最簡(jiǎn)單的收集算法。在經(jīng)歷過(guò)對(duì)象判活以后,我們把需要回收的對(duì)象標(biāo)記出來(lái),然后在統(tǒng)一時(shí)刻回收所有被標(biāo)記的對(duì)象。如圖所示:

黑色標(biāo)記的可回收對(duì)象在回收后全部變成未使用空間,但是這樣回收后有木有發(fā)現(xiàn)空間碎片很多,碎片太多就會(huì)導(dǎo)致再分配稍微大點(diǎn)的空間時(shí),找不到這樣的連續(xù)內(nèi)存,從而導(dǎo)致GC會(huì)被頻繁調(diào)用,所以標(biāo)記清除是一種基礎(chǔ)的垃圾收集算法,其它算法基本都是以它為基礎(chǔ)優(yōu)化產(chǎn)生。
2、復(fù)制算法
復(fù)制算法的思想就是把內(nèi)存分為兩塊,每次只在一邊分配內(nèi)存,當(dāng)一邊的內(nèi)存用完了,就把所有還存活的對(duì)象復(fù)制到另一半去,這時(shí)候把原來(lái)使用過(guò)的這一邊的所有空間一次性清理掉,所以也就不存在內(nèi)存碎片的問(wèn)題了。缺點(diǎn)就是會(huì)浪費(fèi)一半的內(nèi)存空間?;舅悸啡鐖D:

其實(shí)分代GC算法在新生代區(qū)域就用了復(fù)制算法,并且也沒(méi)有分成1:1,而是8:1,也就是所謂的Eden區(qū)和survivor區(qū),新生代中大多數(shù)對(duì)象都是“朝生夕死”的,所以在minorGC時(shí),只把存活下來(lái)的對(duì)象全部復(fù)制到survivor區(qū)。
3、標(biāo)記整理算法
上面提到的復(fù)制算法也有它的弱點(diǎn),就是當(dāng)對(duì)象存活率很高的時(shí)候,就會(huì)存在很多的復(fù)制操作,從而影響了效率。所以這種算法運(yùn)用在老年代的話很明顯不合適,于是又有了標(biāo)記整理算法,這種算法的主要思路就是把活躍對(duì)象標(biāo)記出來(lái),之后再向內(nèi)存的一側(cè)移動(dòng),然后直接清理掉邊界以外的內(nèi)存,具體思路如下:

4、分代收集算法
新生代中的對(duì)象每次回收都基本上只有10%左右的對(duì)象存活,所以需要復(fù)制的對(duì)象很少,效率還不錯(cuò)。實(shí)踐中會(huì)將新生代分為一塊較大的Eden空間和兩塊較小的Surivor空間,每次使用Eden和其中一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著對(duì)象一次地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor空間。HotSpot虛擬機(jī)默認(rèn)的Eden和Survivor的大小比例是8:1:1。也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只有10%的內(nèi)存會(huì)被"浪費(fèi)"。

對(duì)于一個(gè)大型的系統(tǒng),當(dāng)創(chuàng)建的對(duì)象和方法變量比較多時(shí),堆內(nèi)存中的對(duì)象也會(huì)比較多,如果逐一分析對(duì)象是否該回收,那么勢(shì)必造成效率低下。分代收集算法是基于這樣一個(gè)事實(shí):不同的對(duì)象的生命周期(存活情況)是不一樣的,故而不同生命周期的對(duì)象位于堆中不同的區(qū)域,因此對(duì)堆內(nèi)存不同區(qū)域采用不同的策略進(jìn)行回收可以提高JVM的執(zhí)行效率。當(dāng)代商用虛擬機(jī)使用的都是分代收集算法:新生代對(duì)象存活率低,就采用復(fù)制算法;老年代存活率高,就采用標(biāo)記清除算法或者標(biāo)記整理算法。Java堆內(nèi)存一般可以分為新生代、老年代和永久帶三個(gè)模塊。如下所示:

- 1、新生代(Young Generation)
新生代的目標(biāo)是盡可能快速收集掉那些生命周期短的對(duì)象,一般情況下,所有新生成的對(duì)象首先都是放在新生代的。新生代內(nèi)存按照8:1:1的比例分成一個(gè)eden區(qū)和兩個(gè)Survivor(s0,s1)區(qū),大部分對(duì)象在Eden區(qū)中生成。在進(jìn)行垃圾回收時(shí),先將eden區(qū)存活對(duì)象復(fù)制到s0區(qū),然后清空eden區(qū),當(dāng)這個(gè)s0也滿了時(shí),則將eden區(qū)和s0區(qū)存對(duì)象復(fù)制到s1區(qū),然后清空eden和s0。此時(shí)s0區(qū)是空的,然后交換s0區(qū)和s1區(qū)的角色(即下次垃圾回收時(shí)會(huì)掃描Eden區(qū)和s1區(qū)),即保持s0區(qū)為空,如此往返。特別地,當(dāng)s1區(qū)也不足以存放eden區(qū)和s0區(qū)的存活對(duì)象時(shí),就將存活對(duì)象直接存放到老年代。如果老年代也滿了,就會(huì)觸發(fā)一次FullGC,也就是新生代、老年代都進(jìn)行回收。注意,新生代發(fā)生的GC也叫MinorGC,MinorGC發(fā)生頻率比較高,不一定等到Eden區(qū)滿了才觸發(fā)。
- 2、老年代(Old Generation)
老年代存放的都是一些生命周期長(zhǎng)的對(duì)象,就像上面的所敘述的那樣,在新生代中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象就會(huì)被放到老年代中。此外,老年代的內(nèi)存也比新生代大很多,大概比例是(1:2),當(dāng)老年代滿時(shí)會(huì)觸發(fā)Major GC/Full GC,老年代對(duì)象存活時(shí)間比較長(zhǎng),因此Major GC/Full GC發(fā)生的頻率比較低。
- 3、永久代(Permanent Generation)
永久代主要用于存放靜態(tài)文件,如Java類、方法等。永久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如使用反射、動(dòng)態(tài)代理、GCLib等bytecode框架時(shí),在這種時(shí)候需要設(shè)置一個(gè)比較大的永久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類。
- 4、小結(jié)
由于對(duì)象進(jìn)行了分代處理,因此垃圾回收區(qū)域、時(shí)間也不一樣。垃圾回收有兩種類型,Minor GC 和Major GC/Full GC。
Minor GC:對(duì)新生代進(jìn)行回收,不會(huì)影響到老年代。因?yàn)樾律腏ava對(duì)象大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這里使用速度快、效率高的算法,使垃圾回收能盡快完成。
Major GC/Full GC:對(duì)整個(gè)堆進(jìn)行回收,包括新生代和老年代。由于Full GC需要對(duì)整個(gè)堆進(jìn)行回收,所以比Minor GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù),導(dǎo)致Full GC的原因包括:老年代要被寫滿、永久代被寫滿和System.gc()被顯式調(diào)用等。