本文主要介紹java內(nèi)存區(qū)域和GC回收
- java內(nèi)存區(qū)域
- 垃圾收集器
- 參考
java內(nèi)存區(qū)域
運(yùn)行時(shí)內(nèi)存區(qū)域
java虛擬機(jī)在執(zhí)行java程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。
我們注意到運(yùn)行時(shí)區(qū)域主要會(huì)包括5部分區(qū)域,它們有個(gè)各自的用途,以及創(chuàng)建和銷毀時(shí)間,有的依賴虛擬機(jī)進(jìn)程,有的依賴用戶線程。
- 程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用是當(dāng)前線程所執(zhí)行到的字節(jié)碼的位置指示器。字節(jié)碼解釋器工作時(shí)就是通過改變計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,從而達(dá)到分支、循環(huán)、跳轉(zhuǎn)、異常處理等基本功能。
java虛擬機(jī)中的多線程實(shí)際是通過線程輪流切換實(shí)現(xiàn)的。所以實(shí)際上在同一時(shí)刻,處理器的一個(gè)內(nèi)核只會(huì)執(zhí)行一條指令。因此為了線程切換后還能恢復(fù)到正確的執(zhí)行位置,需要每個(gè)線程都要有一個(gè)獨(dú)立的程序計(jì)數(shù)器。而且他們之間互補(bǔ)影響,獨(dú)立工作。所以程序計(jì)數(shù)器是一塊線程私有的內(nèi)存。 - 本地方法棧
與程序計(jì)數(shù)器一樣,本地方法棧也是線程私有的。本地方法棧為虛擬機(jī)提供使用Native方法服務(wù)。由于虛擬機(jī)規(guī)范并沒有對(duì)本地方法棧中的使用語言和數(shù)據(jù)結(jié)構(gòu)等做強(qiáng)制規(guī)定,所以虛擬機(jī)可以自由實(shí)現(xiàn)。 - java虛擬機(jī)棧
同本地方法棧,虛擬機(jī)棧是線程私有,它的生命周期與當(dāng)前線程相同。它為虛擬機(jī)執(zhí)行java方法提供服務(wù)。它描述的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候會(huì)同時(shí)創(chuàng)建一個(gè)棧楨,用于存儲(chǔ)局部變量、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每個(gè)方法的調(diào)用到返回結(jié)果的過程,就是對(duì)應(yīng)一個(gè)棧楨的入棧與出棧。
經(jīng)常有人會(huì)說java內(nèi)存可以粗糙的區(qū)分為堆和棧,這里的棧就是虛擬機(jī)棧,而虛擬機(jī)棧中最重要的就是局部變量表。
局部變量表存放了編譯期可知的基本數(shù)據(jù)類型、對(duì)象的引用(reference類型,它可能只想對(duì)象起始地址的引用指針,也可能指向代表改對(duì)象的句柄)。局部變量表所需要的內(nèi)存在編譯期完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),此方法所需要的內(nèi)存空間大小是確定的,所以在方法運(yùn)行期間,不會(huì)改變局部變量表的大小。 - java堆
對(duì)于虛擬機(jī)來說,堆是其所管理的最大的一塊內(nèi)存。java堆是指被線程共享的一塊內(nèi)存區(qū)域,它在虛擬機(jī)啟動(dòng)時(shí)即創(chuàng)建,堆的唯一目的時(shí)存放對(duì)象實(shí)例。同時(shí)由于堆空間有限,對(duì)象的創(chuàng)建和銷毀是時(shí)常發(fā)生的,所以java堆是垃圾收集器的主要管理區(qū)域,所以java堆有時(shí)也會(huì)稱為GC堆。現(xiàn)在的GC回收基本都采用分代回收算法,所以堆可以細(xì)分為新生代和老年代,新生代又可以分為eden區(qū),from Survivor空間和to Survivor空間等。對(duì)于堆中的各個(gè)區(qū)域分配和回收細(xì)節(jié),在GC部分講解。
在虛擬機(jī)規(guī)范中,沒有強(qiáng)制要求堆是物理內(nèi)存連續(xù)的,只是邏輯上連續(xù)即可。所以當(dāng)前的主流虛擬機(jī)的堆空間都是可以動(dòng)態(tài)擴(kuò)容的,可以通過-Xmx和-Xms控制。 - 方法區(qū)
方法區(qū)同java堆都是線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。java虛擬機(jī)實(shí)現(xiàn)規(guī)范對(duì)該區(qū)域并沒有強(qiáng)制要求實(shí)現(xiàn)GC回收,所以相對(duì)而言,該區(qū)域的垃圾收集器很少出現(xiàn),所以有人開發(fā)者會(huì)成稱該區(qū)域?yàn)橛谰么_@個(gè)區(qū)域的內(nèi)存回收主要是針對(duì)常量池的回收和對(duì)類型的卸載。
運(yùn)行時(shí)常量池
一個(gè)class文件除了有類的版本、字段、方法、接口等描述以外,還有一項(xiàng)是常量池,用于存放編譯期間生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。運(yùn)行時(shí)常量池是具有動(dòng)態(tài)性的,java虛擬機(jī)對(duì)class文件的每一部分的格式有嚴(yán)格的規(guī)定,每個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都有規(guī)范要求,這樣才會(huì)被虛擬機(jī)認(rèn)可。但是對(duì)于常量池是比較寬松的,因?yàn)閖ava并不要求常量一定要編譯期產(chǎn)生,也可以在運(yùn)行期間放入常量,比如String的intern()方法。
對(duì)象訪問
在java虛擬機(jī)棧中我們提到局部變量表存放了對(duì)象的引用,我們都知道對(duì)象是分配的java堆中的,那么具體是怎么引用的呢?
比如Object obj = new Object();,假設(shè)這句代碼出現(xiàn)在方法體中,那么“Object obj ”這部分語義將會(huì)反映到j(luò)ava棧的本地變量表中(為reference類型),而“new Object()”這部分語義將會(huì)反映在java堆上,形成一塊存儲(chǔ)了Object類型所有實(shí)例數(shù)據(jù)值的結(jié)構(gòu)化內(nèi)存。
由于reference類型在java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫栽趯?shí)際虛擬機(jī)中訪問會(huì)有所不同,主流訪問有兩種:
- 句柄訪問
- 直接指針訪問
句柄訪問
java堆會(huì)劃分出一小塊內(nèi)存空間作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,二句柄中包含了對(duì)象實(shí)例數(shù)據(jù)和類型數(shù)據(jù)的各自地址信息。
直接地址訪問
reference中直接存儲(chǔ)的就是對(duì)象的地址,java堆需要考慮對(duì)象的布局中如何存放訪問類型數(shù)據(jù)的相關(guān)信息。
這兩種對(duì)象的訪問方式各有優(yōu)勢(shì),使用句柄訪問方式的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的的時(shí)間開銷。
垃圾收集器
判斷對(duì)象死亡
GC在對(duì)堆內(nèi)存進(jìn)行回收前,第一件事是需要確定哪些對(duì)象是需要被回收的,所以就需要判斷對(duì)象是否存活。一般的有兩種方法來判斷:
- 引用計(jì)數(shù)法
給一個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有地方對(duì)其引用時(shí),計(jì)數(shù)器加1,當(dāng)引用實(shí)效時(shí),計(jì)數(shù)器減1,任何時(shí)刻計(jì)數(shù)器為0時(shí)就表示該對(duì)象不再被使用。
引用計(jì)數(shù)法實(shí)現(xiàn)簡(jiǎn)單,通常是比較高效的,但是引用計(jì)數(shù)法有個(gè)弊端是當(dāng)兩個(gè)不再被使用的對(duì)象互相引用時(shí),導(dǎo)致兩者都不會(huì)被釋放。 - 根搜索算法
根搜索算法是指通過一系列名為“GC roots"的對(duì)象為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索過的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC roots沒有任何引用鏈相連,就表示此對(duì)象不再被使用。
在java語言中,作為GC roots的對(duì)象包括以下幾種:
a. java虛擬機(jī)棧(棧楨中本地變量表)中引用的對(duì)象
b. 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
c. 方法區(qū)中常量引用的對(duì)象
d. 本地方法棧中JNI引用的對(duì)象
方法區(qū)回收
前面已經(jīng)提到方法區(qū)是很少出現(xiàn)垃圾收集器的,因?yàn)榉椒▍^(qū)回收的性價(jià)比比較低,通常堆內(nèi)存的回收一次可以回收70%-95%的空間,但方法區(qū)的垃圾收集器效率很低。
一般的,方法區(qū)回收主要由兩部分:
1.廢棄常量
廢棄的常量與堆回收比較類似,只需要指導(dǎo)該常量是否在其他地方被使用即可。
2.無用的類
這種情況的判斷比較苛刻,一般要求滿足以下三個(gè)條件才算是無用的:
a. 該類的所有實(shí)例都被回收
b. 加載該類的ClassLoader也被回收
c. 該類對(duì)應(yīng)的java.lang.class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類
GC回收算法
1.標(biāo)記-清除算法
最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為“標(biāo)記”和“清除”兩個(gè)階段。
a. 首先標(biāo)記出所有需要回收的對(duì)象
b. 在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
缺點(diǎn):
效率問題:標(biāo)記和清除兩個(gè)過程的效率都不高
空間問題:標(biāo)記清除之后產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
2.復(fù)制算法
目的是為了解決效率問題。
將可用內(nèi)存按容量大小劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)一塊內(nèi)存使用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況。
缺點(diǎn): 將內(nèi)存縮小為了原來的一半。
現(xiàn)代的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中對(duì)象98%對(duì)象是“朝生夕死”的,所以不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
3.標(biāo)記-整理算法
復(fù)制收集算法在對(duì)象存活率較高時(shí),就要進(jìn)行較多的復(fù)制操作,效率就會(huì)變低。 根據(jù)老年代的特點(diǎn),提出了“標(biāo)記-整理”算法。
標(biāo)記過程仍然與”標(biāo)記-清除“算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
4.分代收集算法
一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法。在老年代中,因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須采用“標(biāo)記-清除”或“標(biāo)記-整理”算法來進(jìn)行回收。JVM把年輕代分為了三部分:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫from和to),默認(rèn)比例為8:1。
工作過程:一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(一些大對(duì)象特殊處理),這些對(duì)象經(jīng)過第一次GC后,如果仍然存活,將會(huì)被移到Survivor區(qū)。對(duì)象在Survivor區(qū)中每熬過一次GC,年齡就會(huì)增加1歲,當(dāng)它的年齡增加到一定程度時(shí),就會(huì)被移動(dòng)到年老代中。 因?yàn)槟贻p代中的對(duì)象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是復(fù)制算法,復(fù)制算法不會(huì)產(chǎn)生內(nèi)存碎片。在GC開始的時(shí)候,對(duì)象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會(huì)保證名為To的Survivor區(qū)域是空的。GC會(huì)一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會(huì)將所有對(duì)象移動(dòng)到年老代中。
空間分配擔(dān)保
先了解下Minor GC與Major GC/Full GC
- Minor GC
即新生代GC,指發(fā)生在新生代的垃圾收集動(dòng)作,Minor GC的回收的對(duì)象大多具備朝生夕滅的特性,所以Minor GC是非常頻繁,并且回收速度比較快。 - Major GC/Full GC
即老年代GC,指發(fā)生在老年代的垃圾收集動(dòng)作,出現(xiàn)Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC。Major GC的速度一般比Minor GC慢10倍以上。
在發(fā)生Minor GC時(shí),虛擬機(jī)會(huì)檢測(cè)之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于,則改為直接進(jìn)行一次Full GC,如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失敗,如果允許,那么只會(huì)進(jìn)行Minor GC,如果不允許,那么進(jìn)行一次Full GC。
在分代回收算法中提到過,新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個(gè)Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活),就需要老年代進(jìn)行分配擔(dān)保,把Survivor無法容納的對(duì)象直接進(jìn)入老年代。與生活中的貸款擔(dān)保類似,老年代要進(jìn)行這樣的擔(dān)保,前提是老年代本身還有容納這些對(duì)象的剩余空間,一共有多少對(duì)象會(huì)活下來在實(shí)際完成內(nèi)存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對(duì)象容量的平均大小值作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC來讓老年代騰出更多空間。
參考
- 深入理解java虛擬機(jī) [第三版](周志明)
- JVM 垃圾回收器工作原理及使用實(shí)例介紹
- 搞定JVM垃圾回收
- JVM內(nèi)存模型