Java虛擬機(jī):內(nèi)存模型詳解
我們都知道,當(dāng)虛擬機(jī)執(zhí)行Java代碼的時(shí)候,首先要把字節(jié)碼文件加載到內(nèi)存,那么這些類的信息都存放在內(nèi)存中的哪個(gè)區(qū)域呢?當(dāng)我們創(chuàng)建一個(gè)對象實(shí)例的時(shí)候,虛擬機(jī)要為對象分配內(nèi)存,Java虛擬機(jī)又是如何配分內(nèi)存的呢?這些都涉及到Java虛擬機(jī)的內(nèi)存劃分機(jī)制,今天我們就來探究一下Java虛擬機(jī)的內(nèi)存模型。
Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域都有各自的用途以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域則依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存包括以下幾個(gè)數(shù)據(jù)區(qū)域。如下圖所示:
以上就是Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的劃分,每一塊內(nèi)存區(qū)域都有它的職責(zé),存放著不同的運(yùn)行時(shí)數(shù)據(jù)。
虛擬機(jī)棧
Java虛擬機(jī)棧是線程私有的,每一個(gè)線程在這個(gè)區(qū)域都有一塊所屬的內(nèi)存區(qū)域,它的生命周期與線程相同,隨線程啟動(dòng)而生,隨線程消亡而滅。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型,每一個(gè)線程都對應(yīng)著虛擬機(jī)棧區(qū)域里的一個(gè)棧數(shù)據(jù)結(jié)構(gòu),由于一個(gè)線程的方法調(diào)用鏈可能會(huì)很長,每一個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀,棧幀就是線程對應(yīng)的棧數(shù)據(jù)結(jié)構(gòu)的棧元素,棧幀用于存儲局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接等信息。局部變量表存放了方法參數(shù)和方法內(nèi)部定義的局部變量,包括各種基本數(shù)據(jù)類型和對象引用類型等信息。經(jīng)常聽到有的程序猿粗糙的把虛擬機(jī)內(nèi)存劃分為堆內(nèi)存和棧內(nèi)存,這種劃分只能說明大多數(shù)程序猿比較關(guān)注的、與對象內(nèi)存分配關(guān)系最密切的是這兩塊內(nèi)存區(qū)域,其中的“棧內(nèi)存”就是這里所說的虛擬機(jī)棧,而虛擬機(jī)棧里我們程序猿最為關(guān)注的就是局部變量表部分。
本地方法棧
本地方法棧也是線程私有的,它與虛擬機(jī)棧發(fā)揮的作用相似,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法(本地方法)服務(wù)。在Java虛擬機(jī)規(guī)范中并沒有對本地方法棧的實(shí)現(xiàn)做強(qiáng)制規(guī)定,有的虛擬機(jī)甚至直接把虛擬機(jī)棧和本地方法棧合二為一。
堆
Java堆是所有線程所共享的一塊內(nèi)存區(qū)域,也是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。這塊內(nèi)存的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。Java堆是垃圾收集器管理的主要區(qū)域,從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆還可以細(xì)分為新生代和老年代。新生代還可以再細(xì)分為Eden區(qū)域、FromSurvivor區(qū)域和ToSurvivor區(qū)域。無論怎么劃分,都與存放的內(nèi)容無關(guān),存儲的任然都是對象實(shí)例。進(jìn)一步劃分的目的是為了更好的回收或者更快的分配內(nèi)存。
方法區(qū)
方法區(qū)與Java堆一樣,是線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)是一個(gè)邏輯區(qū),具體屬于哪一塊物理內(nèi)存根據(jù)不同的虛擬機(jī)實(shí)現(xiàn)而定。在HotSpot的實(shí)現(xiàn)中,方法區(qū)邏輯上與堆內(nèi)存隔離,物理存儲上卻是是屬于Java堆的一部分。很多人把方法區(qū)稱為“永久代”,其實(shí)是HotSpot使用永久代來實(shí)現(xiàn)方法區(qū)而已。其他的虛擬機(jī)實(shí)現(xiàn)并沒有永久代這一概念。Java虛擬機(jī)規(guī)范對方法區(qū)的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾回收。一般來說不會(huì)在方法區(qū)進(jìn)行垃圾回收,在這一區(qū)域進(jìn)行回收的效果很難讓人滿意。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)會(huì)拋出內(nèi)存溢出異常。
運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存放編譯期間生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存儲。
程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊很小的內(nèi)存空間,它也是線程私有的。它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,通過改變這個(gè)計(jì)數(shù)器的值來選取下一行需要執(zhí)行的字節(jié)碼指令。由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器內(nèi)核只會(huì)執(zhí)行一條線程中的指令,因此,為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間互不影響,獨(dú)立存儲。
以上就是Java虛擬機(jī)的內(nèi)存模型劃分,這是我們程序猿必須掌握的原理,弄清Java虛擬機(jī)的內(nèi)存模型,是理解虛擬機(jī)內(nèi)存分配和垃圾回收的基礎(chǔ),以此作為總結(jié)。
Java虛擬機(jī):如何判定哪些對象可回收?
在堆內(nèi)存中存放著Java程序中幾乎所有的對象實(shí)例,堆內(nèi)存的容量是有限的,Java虛擬機(jī)會(huì)對堆內(nèi)存進(jìn)行管理,回收已經(jīng)“死去”的對象(即不可能再被任何途徑使用的對象),釋放內(nèi)存。垃圾收集器在對堆內(nèi)存進(jìn)行回收前,首先要做的第一件事就是確定這些對象中哪些還存活著,哪些已經(jīng)死去。Java虛擬機(jī)是如何判斷對象是否可以被回收的呢?
引用計(jì)數(shù)算法
引用計(jì)數(shù)算法的原理是這樣的:給對象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;在任何時(shí)刻計(jì)數(shù)器的值為0的對象就是不可能再被使用的,也就是可被回收的對象。
引用計(jì)數(shù)算法的效率很高,但是主流的JVM并沒有選用這種算法來判定可回收對象,因?yàn)樗幸粋€(gè)致命的缺陷,那就是它無法解決對象之間相互循環(huán)引用的的問題,對于循環(huán)引用的對象它無法進(jìn)行回收。
假設(shè)有這樣一段代碼:
public class Object {
?
public Object instance;
?
public static void main(String[] args) {
?
// 1
Object objectA = new Object();
Object objectB = new Object();
?
// 2
objectA.instance = objectB;
objectB.instance = objectA;
?
// 3
objectA = null;
objectB = null;
?
}
程序啟動(dòng)后,objectA和objectB兩個(gè)對象被創(chuàng)建并在堆中分配內(nèi)存,這兩個(gè)對象都相互持有對方的引用,除此之外,這兩個(gè)對象再無任何其他引用,實(shí)際上這兩個(gè)對象已經(jīng)不可能再被訪問(引用被置空,無法訪問),但是它們因?yàn)橄嗷ヒ弥鴮Ψ?,?dǎo)致它們的引用計(jì)數(shù)器都不為0,于是引用計(jì)數(shù)算法無法通知GC收集器回收它們。
實(shí)際上,當(dāng)?shù)?步執(zhí)行時(shí),兩個(gè)對象的引用計(jì)數(shù)器值都為1;當(dāng)?shù)?步執(zhí)行時(shí),兩個(gè)對象的引用計(jì)數(shù)器都為2;當(dāng)?shù)?步執(zhí)行時(shí),二者都清為空值,引用計(jì)數(shù)器值都變?yōu)?。根據(jù)引用計(jì)數(shù)算法的思想,值不為0的對象被認(rèn)為是存活的,不會(huì)被回收;而事實(shí)上這兩個(gè)對象已經(jīng)不可能再被訪問了,應(yīng)該被回收。
可達(dá)性分析算法
在主流的JVM實(shí)現(xiàn)中,都是通過可達(dá)性分析算法來判定對象是否存活的??蛇_(dá)性分析算法的基本思想是:通過一系列被稱為"GC Roots"的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索走過的路徑稱為引用鏈,當(dāng)一個(gè)對象到GC Roots對象沒有任何引用鏈相連,就認(rèn)為GC Roots到這個(gè)對象是不可達(dá)的,判定此對象為不可用對象,可以被回收。在上圖中,objectA、objectB、objectC是可達(dá)的,不會(huì)被回收;objectD、objectE雖然有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的,所以它們將會(huì)被判定為是可回收的對象。
在Java中,可作為GC Roots的對象包括下面幾種:
1、虛擬機(jī)棧中引用的對象;
2、方法區(qū)中類靜態(tài)屬性引用的對象;
3、方法區(qū)中常量引用的對象;
4、本地方法棧中Native方法引用的對象。
Java虛擬機(jī):GC算法深度解析
在上面的內(nèi)容里里介紹了可達(dá)性分析算法,它為我們解決了判定哪些對象可以回收的問題,接下來就該我們的垃圾收集算法出場了。不同的垃圾收集算法有各自不同的優(yōu)缺點(diǎn),在JVM實(shí)現(xiàn)中,往往不是采用單一的一種算法進(jìn)行回收,而是采用幾種不同的算法組合使用,來達(dá)到最好的收集效果。接下來詳細(xì)介紹幾種垃圾收集算法的思想及發(fā)展過程。
最基礎(chǔ)的收集算法 —— 標(biāo)記/清除算法
之所以說標(biāo)記/清除算法是幾種GC算法中最基礎(chǔ)的算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對其不足進(jìn)行改進(jìn)而得到的。標(biāo)記/清除算法的基本思想就跟它的名字一樣,分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。
標(biāo)記階段:標(biāo)記的過程其實(shí)就是前面介紹的可達(dá)性分析算法的過程,遍歷所有的GC Roots對象,對從GC Roots對象可達(dá)的對象都打上一個(gè)標(biāo)識,一般是在對象的header中,將其記錄為可達(dá)對象;
清除階段:清除的過程是對堆內(nèi)存進(jìn)行遍歷,如果發(fā)現(xiàn)某個(gè)對象沒有被標(biāo)記為可達(dá)對象(通過讀取對象header信息),則將其回收。
上圖是標(biāo)記/清除算法的示意圖,在標(biāo)記階段,從對象GC Root 1可以訪問到B對象,從B對象又可以訪問到E對象,因此從GC Root 1到B、E都是可達(dá)的,同理,對象F、G、J、K都是可達(dá)對象;到了清除階段,所有不可達(dá)對象都會(huì)被回收。
在垃圾收集器進(jìn)行GC時(shí),必須停止所有Java執(zhí)行線程(也稱"Stop The World"),原因是在標(biāo)記階段進(jìn)行可達(dá)性分析時(shí),不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,否則的話可達(dá)性分析結(jié)果的準(zhǔn)確性就無法得到保證。在等待標(biāo)記清除結(jié)束后,應(yīng)用線程才會(huì)恢復(fù)運(yùn)行。
前面剛提過,后續(xù)的收集算法是在標(biāo)記/清除算法的基礎(chǔ)上進(jìn)行改進(jìn)而來的,那也就是說標(biāo)記/清除算法有它的不足。其實(shí)了解了它的原理,其缺點(diǎn)也就不難看出了。
1、效率問題。標(biāo)記和清除兩個(gè)階段的效率都不高,因?yàn)檫@兩個(gè)階段都需要遍歷內(nèi)存中的對象,很多時(shí)候內(nèi)存中的對象實(shí)例數(shù)量是非常龐大的,這無疑很耗費(fèi)時(shí)間,而且GC時(shí)需要停止應(yīng)用程序,這會(huì)導(dǎo)致非常差的用戶體驗(yàn)。
2、空間問題。標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片(從上圖可以看出),內(nèi)存空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動(dòng)作。
既然標(biāo)記/清除算法有這么多的缺點(diǎn),那它還有存在的意義嗎?別急,一個(gè)算法有缺陷,人們肯定會(huì)想辦法去完善它,接下來的兩個(gè)算法就是在標(biāo)記/清除算法的基礎(chǔ)上完善而來的。
復(fù)制算法
為了解決效率問題,復(fù)制算法出現(xiàn)了。復(fù)制算法的原理是:將可用內(nèi)存按容量劃分為大小相等的兩塊,每次使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活的對象復(fù)制到另一塊內(nèi)存上,然后把這一塊內(nèi)存所有的對象一次性清理掉。用圖說明如下:
回收前:
回收后:
復(fù)制算法每次都是對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,這樣就減少了標(biāo)記對象遍歷的時(shí)間,在清除使用區(qū)域?qū)ο髸r(shí),不用進(jìn)行遍歷,直接清空整個(gè)區(qū)域內(nèi)存,而且在將存活對象復(fù)制到保留區(qū)域時(shí)也是按地址順序存儲的,這樣就解決了內(nèi)存碎片的問題,在分配對象內(nèi)存時(shí)不用考慮內(nèi)存碎片等復(fù)雜問題,只需要按順序分配內(nèi)存即可。
復(fù)制算法簡單高效,優(yōu)化了標(biāo)記/清除算法的效率低、內(nèi)存碎片多的問題。但是它的缺點(diǎn)也很明顯:
1、將內(nèi)存縮小為原來的一半,浪費(fèi)了一半的內(nèi)存空間,代價(jià)太高;
2、如果對象的存活率很高,極端一點(diǎn)的情況假設(shè)對象存活率為100%,那么我們需要將所有存活的對象復(fù)制一遍,耗費(fèi)的時(shí)間代價(jià)也是不可忽視的。
基于以上復(fù)制算法的缺點(diǎn),由于新生代中的對象幾乎都是“朝生夕死”的(達(dá)到98%),現(xiàn)在的商業(yè)虛擬機(jī)都采用復(fù)制算法來回收新生代。由于新生代的對象存活率低,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的From Survivor空間、To Survivor空間,三者的比例為8:1:1。每次使用Eden和From Survivor區(qū)域,To Survivor作為保留空間。
GC開始時(shí),對象只會(huì)存在于Eden區(qū)和From Survivor區(qū),To Survivor區(qū)是空的。GC進(jìn)行時(shí),Eden區(qū)中所有存活的對象都會(huì)被復(fù)制到To Survivor區(qū),而在From Survivor區(qū)中,仍存活的對象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閥值(默認(rèn)為15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1)的對象會(huì)被移到老年代中,沒有達(dá)到閥值的對象會(huì)被復(fù)制到To Survivor區(qū)。接著清空Eden區(qū)和From Survivor區(qū),新生代中存活的對象都在To Survivor區(qū)。接著, From Survivor區(qū)和To Survivor區(qū)會(huì)交換它們的角色,也就是新的ToSurvivor區(qū)就是上次GC清空的From Survivor區(qū),新的From Survivor區(qū)就是上次GC的ToSurvivor區(qū),
總之,不管怎樣都會(huì)保證To Survivor區(qū)在一輪GC后是空的。GC時(shí)當(dāng)To Survivor區(qū)沒有足夠的空間存放上一次新生代收集下來的存活對象時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保,將這些對象存放在老年代中。
標(biāo)記/整理算法
復(fù)制算法在對象存活率較高時(shí)要進(jìn)行較多的復(fù)制操作,效率會(huì)變得很低,更關(guān)鍵的是,如果不想浪費(fèi)50%的內(nèi)存空間,就需要有額外的內(nèi)存空間進(jìn)行分配擔(dān)保,以應(yīng)對內(nèi)存中對象100%存活的極端情況,因此,在老年代中由于對象的存活率非常高,復(fù)制算法就不合適了。根據(jù)老年代的特點(diǎn),高人們提出了另一種算法:標(biāo)記/整理算法。從名字上看,這種算法與標(biāo)記/清除算法很像,事實(shí)上,標(biāo)記/整理算法的標(biāo)記過程任然與標(biāo)記/清除算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行回收,而是讓所有存活的對象都向一端移動(dòng),然后直接清理掉端邊線以外的內(nèi)存。
回收前:
回收后:
可以看到,回收后可回收對象被清理掉了,存活的對象按規(guī)則排列存放在內(nèi)存中。這樣一來,當(dāng)我們給新對象分配內(nèi)存時(shí),jvm只需要持有內(nèi)存的起始地址即可。標(biāo)記/整理算法不僅彌補(bǔ)了標(biāo)記/清除算法存在內(nèi)存碎片的問題,也消除了復(fù)制算法內(nèi)存減半的高額代價(jià),可謂一舉兩得。但任何算法都有缺點(diǎn),就像人無完人,標(biāo)記/整理算法的缺點(diǎn)就是效率也不高,不僅要標(biāo)記存活對象,還要整理所有存活對象的引用地址,在效率上不如復(fù)制算法。
弄清了以上三種算法的原理,下面我們來從幾個(gè)方面對這幾種算法做一個(gè)簡單排行。
效率:復(fù)制算法 > 標(biāo)記/整理算法 > 標(biāo)記/清除算法(標(biāo)記/清除算法有內(nèi)存碎片問題,給大對象分配內(nèi)存時(shí)可能會(huì)觸發(fā)新一輪垃圾回收)
內(nèi)存整齊率:復(fù)制算法 = 標(biāo)記/整理算法 > 標(biāo)記/清除算法
內(nèi)存利用率:標(biāo)記/整理算法 = 標(biāo)記/清除算法 > 復(fù)制算法
從上面簡單的評估可以看出,標(biāo)記/清除算法已經(jīng)比較落后了,但是吃水不忘挖井人,它是后面幾種算法的前輩、是基礎(chǔ),在某些場景下它也有用武之地。
終極算法 —— 分代收集算法
當(dāng)前商業(yè)虛擬機(jī)都采用分代收集算法,說它是終極算法,是因?yàn)樗Y(jié)合了前幾種算法的優(yōu)點(diǎn),將算法組合使用進(jìn)行垃圾回收,與其說它是一種新的算法,不如說它是對前幾種算法的實(shí)際應(yīng)用。分代收集算法的思想是按對象的存活周期不同將內(nèi)存劃分為幾塊,一般是把Java堆分為新生代和老年代(還有一個(gè)永久代,是HotSpot特有的實(shí)現(xiàn),其他的虛擬機(jī)實(shí)現(xiàn)沒有這一概念,永久代的收集效果很差,一般很少對永久代進(jìn)行垃圾回收),這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最合適的收集算法。
新生代:朝生夕滅,存活時(shí)間很短。
老年代:經(jīng)過多次Minor GC而存活下來,存活周期長。
在新生代中每次垃圾回收都發(fā)現(xiàn)有大量的對象死去,只有少量存活,因此采用復(fù)制算法回收新生代,只需要付出少量對象的復(fù)制成本就可以完成收集;而老年代中對象的存活率高,不適合采用復(fù)制算法,而且如果老年代采用復(fù)制算法,它是沒有額外的空間進(jìn)行分配擔(dān)保的,因此必須使用標(biāo)記/清理算法或者標(biāo)記/整理算法來進(jìn)行回收。
總結(jié)一下就是,分代收集算法的原理是采用復(fù)制算法來收集新生代,采用標(biāo)記/清理算法或者標(biāo)記/整理算法收集老年代。
以上內(nèi)容介紹了幾種收集算法的原理、優(yōu)缺點(diǎn)以及使用場景,它們的共同點(diǎn)是:當(dāng)GC線程啟動(dòng)時(shí)(即進(jìn)行垃圾收集),應(yīng)用程序都要暫停(Stop The World)。理解了這些知識,為我們研究垃圾收集器的運(yùn)行原理打下了基礎(chǔ)。
Java虛擬機(jī):JVM內(nèi)存分代策略
Java虛擬機(jī)根據(jù)對象存活的周期不同,把堆內(nèi)存劃分為幾塊,一般分為新生代、老年代和永久代(對HotSpot虛擬機(jī)而言),這就是JVM的內(nèi)存分代策略。
為什么要分代?
堆內(nèi)存是虛擬機(jī)管理的內(nèi)存中最大的一塊,也是垃圾回收最頻繁的一塊區(qū)域,我們程序所有的對象實(shí)例都存放在堆內(nèi)存中。給堆內(nèi)存分代是為了提高對象內(nèi)存分配和垃圾回收的效率。試想一下,如果堆內(nèi)存沒有區(qū)域劃分,所有的新創(chuàng)建的對象和生命周期很長的對象放在一起,隨著程序的執(zhí)行,堆內(nèi)存需要頻繁進(jìn)行垃圾收集,而每次回收都要遍歷所有的對象,遍歷這些對象所花費(fèi)的時(shí)間代價(jià)是巨大的,會(huì)嚴(yán)重影響我們的GC效率,這簡直太可怕了。
有了內(nèi)存分代,情況就不同了,新創(chuàng)建的對象會(huì)在新生代中分配內(nèi)存,經(jīng)過多次回收仍然存活下來的對象存放在老年代中,靜態(tài)屬性、類信息等存放在永久代中,新生代中的對象存活時(shí)間短,只需要在新生代區(qū)域中頻繁進(jìn)行GC,老年代中對象生命周期長,內(nèi)存回收的頻率相對較低,不需要頻繁進(jìn)行回收,永久代中回收效果太差,一般不進(jìn)行垃圾回收,還可以根據(jù)不同年代的特點(diǎn)采用合適的垃圾收集算法。分代收集大大提升了收集效率,這些都是內(nèi)存分代帶來的好處。
內(nèi)存分代劃分
Java虛擬機(jī)將堆內(nèi)存劃分為新生代、老年代和永久代,永久代是HotSpot虛擬機(jī)特有的概念,它采用永久代的方式來實(shí)現(xiàn)方法區(qū),其他的虛擬機(jī)實(shí)現(xiàn)沒有這一概念,而且HotSpot也有取消永久代的趨勢,在JDK 1.7中HotSpot已經(jīng)開始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、類信息、靜態(tài)變量等數(shù)據(jù),與垃圾回收關(guān)系不大,新生代和老年代是垃圾回收的主要區(qū)域。內(nèi)存分代示意圖如下:
新生代(Young)
新生成的對象優(yōu)先存放在新生代中,新生代對象朝生夕死,存活率很低,在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。
HotSpot將新生代劃分為三塊,一塊較大的Eden空間和兩塊較小的Survivor空間,默認(rèn)比例為8:1:1。劃分的目的是因?yàn)镠otSpot采用復(fù)制算法來回收新生代,設(shè)置這個(gè)比例是為了充分利用內(nèi)存空間,減少浪費(fèi)。新生成的對象在Eden區(qū)分配(大對象除外,大對象直接進(jìn)入老年代),當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。
GC開始時(shí),對象只會(huì)存在于Eden區(qū)和From Survivor區(qū),To Survivor區(qū)是空的(作為保留區(qū)域)。GC進(jìn)行時(shí),Eden區(qū)中所有存活的對象都會(huì)被復(fù)制到To Survivor區(qū),而在From Survivor區(qū)中,仍存活的對象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閥值(默認(rèn)為15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1,GC分代年齡存儲在對象的header中)的對象會(huì)被移到老年代中,沒有達(dá)到閥值的對象會(huì)被復(fù)制到To Survivor區(qū)。接著清空Eden區(qū)和From Survivor區(qū),新生代中存活的對象都在To Survivor區(qū)。接著, From Survivor區(qū)和To Survivor區(qū)會(huì)交換它們的角色,也就是新的To Survivor區(qū)就是上次GC清空的From Survivor區(qū),新的From Survivor區(qū)就是上次GC的To Survivor區(qū),總之,不管怎樣都會(huì)保證To Survivor區(qū)在一輪GC后是空的。GC時(shí)當(dāng)To Survivor區(qū)沒有足夠的空間存放上一次新生代收集下來的存活對象時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保,將這些對象存放在老年代中。
老年代(Old)
在新生代中經(jīng)歷了多次(具體看虛擬機(jī)配置的閥值)GC后仍然存活下來的對象會(huì)進(jìn)入老年代中。老年代中的對象生命周期較長,存活率比較高,在老年代中進(jìn)行GC的頻率相對而言較低,而且回收的速度也比較慢。
永久代(Permanent)
永久代存儲類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),對這一區(qū)域而言,Java虛擬機(jī)規(guī)范指出可以不進(jìn)行垃圾收集,一般而言不會(huì)進(jìn)行垃圾回收。
Minor GC 和 Full GC的區(qū)別
新生代GC(Minor GC):Minor GC指發(fā)生在新生代的GC,因?yàn)樾律腏ava對象大多都是朝生夕死,所以Minor GC非常頻繁,一般回收速度也比較快。當(dāng)Eden空間不足以為對象分配內(nèi)存時(shí),會(huì)觸發(fā)Minor GC。
老年代GC(Full GC/Major GC):Full GC指發(fā)生在老年代的GC,出現(xiàn)了Full GC一般會(huì)伴隨著至少一次的Minor GC(老年代的對象大部分是Minor GC過程中從新生代進(jìn)入老年代),比如:分配擔(dān)保失敗。Full GC的速度一般會(huì)比Minor GC慢10倍以上。當(dāng)老年代內(nèi)存不足或者顯式調(diào)用System.gc()方法時(shí),會(huì)觸發(fā)Full GC。
Java虛擬機(jī):類加載機(jī)制詳解
大家知道,我們的Java程序被編譯器編譯成class文件,在class文件中描述的各種信息,最終都需要加載到虛擬機(jī)內(nèi)存才能運(yùn)行和使用,那么虛擬機(jī)是如何加載這些class文件的呢?在加載class文件的過程中虛擬機(jī)又干了哪些事呢?今天我們來解密虛擬機(jī)的類加載機(jī)制。
虛擬機(jī)把class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型(Class對象),這就是虛擬機(jī)的類加載機(jī)制。
類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段。,其中驗(yàn)證、準(zhǔn)備和解析3個(gè)階段統(tǒng)稱為連接階段。如圖:前面的5個(gè)階段就是類加載的過程。其中加載、驗(yàn)證、準(zhǔn)備和初始化這幾個(gè)階段的順序是確定的,而解析階段則不一定,在某些情況下它可以在初始化階段以后才進(jìn)行。那么,在類加載的每一個(gè)步驟中,虛擬機(jī)都進(jìn)行了那些工作呢?
加載
加載是類加載過程的第一個(gè)階段,在這一階段,虛擬機(jī)主要完成了3件事:
1、通過類的全限定名來獲取定義這個(gè)類的二進(jìn)制字節(jié)流。簡單來說就是,通過類的包名加類名來定位到此類的class文件的位置,相當(dāng)于一個(gè)資源定位的過程。
2、將這個(gè)字節(jié)流代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。也就是將類中定義的靜態(tài)變量、常量等信息存儲在方法區(qū)中。
3、在堆內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為方法區(qū)中這個(gè)類的各種數(shù)據(jù)的訪問入口。
總結(jié)一下,加載階段的主要工作就是,把class二進(jìn)制文件加載到內(nèi)存后,將類中定義的靜態(tài)變量、常量、類信息等數(shù)據(jù)存放到方法區(qū),并在堆內(nèi)存中創(chuàng)建一個(gè)代表這個(gè)類的Class對象,作為方法區(qū)中這個(gè)類的數(shù)據(jù)信息的訪問入口,程序猿可以持有這個(gè)Class對象。
驗(yàn)證
驗(yàn)證是連接階段的第一步,驗(yàn)證階段的目的是確保class文件中包含的信息符合虛擬機(jī)的要求,并且不會(huì)危害到虛擬機(jī)自身的安全。驗(yàn)證的內(nèi)容主要包含以下幾個(gè)方面:
1、文件格式驗(yàn)證。主要目的是保證輸入的字節(jié)流能正確的解析并存儲在方法區(qū)中,格式上符合一個(gè)Java類型信息的要求。這個(gè)階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過了這個(gè)階段的驗(yàn)證,字節(jié)流才能進(jìn)入方法區(qū)進(jìn)行存儲,所有后面的3個(gè)階段的驗(yàn)證都是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的,不會(huì)直接操作字節(jié)流。
2、元數(shù)據(jù)驗(yàn)證。這一階段的主要目的是對類的元數(shù)據(jù)(定義數(shù)據(jù)的數(shù)據(jù))信息進(jìn)行語義校驗(yàn),確保不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。包括:該類是否有父類、該類的父類是否繼承了不允許被繼承的類、該類中的字段和方法是否與父類產(chǎn)生矛盾等等。
3、字節(jié)碼驗(yàn)證。目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是否合法、符合邏輯。在第二階段對元數(shù)據(jù)信息的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對類的方法體進(jìn)行分析,保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)危害虛擬機(jī)的安全。
4、符號引用驗(yàn)證。符號引用驗(yàn)證發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作是在連接的第三階段——解析階段中進(jìn)行的。符號引用驗(yàn)證的目的是確保解析動(dòng)作能夠正常執(zhí)行。
對于類加載機(jī)制而言,驗(yàn)證階段是一個(gè)非常重要、但不是一定必要的階段。如果所運(yùn)行的全部代碼都已經(jīng)被反復(fù)使用和驗(yàn)證過了,那么就可以使用虛擬機(jī)參數(shù)-Xverify:none來關(guān)閉大部分的類驗(yàn)證措施,以縮短類加載的時(shí)間。
準(zhǔn)備
準(zhǔn)備階段的主要工作是為類的靜態(tài)變量分配內(nèi)存并設(shè)置變量的初始默認(rèn)值。這些變量所使用的內(nèi)存都在方法區(qū)中分配。這里有兩個(gè)問題需要說明:
1、這一階段進(jìn)行內(nèi)存分配的僅包括靜態(tài)變量,而不包括實(shí)例變量(靜態(tài)變量是所有對象共有的,實(shí)例變量是對象私有的),實(shí)例變量將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中。
2、這里說的為對象賦初始值是各數(shù)據(jù)類型對應(yīng)的零值。假設(shè)有一個(gè)靜態(tài)變量定義為public static int a = 1; 那變量a的初始值就是0而不是1,初始值1在初始化階段賦給變量a。如果是引用類型初始默認(rèn)值就是null。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
符號引用:符號引用以一組符號來描述所引用的目標(biāo),符號可以使任何形式的字面量,只要使用時(shí)能無歧義的定位到目標(biāo)即可。符號引用的字面量形式在Java虛擬機(jī)規(guī)范的Class文件格式中有明確定義。
直接引用:直接引用可以是直接指向目標(biāo)的指針、相對偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局有關(guān)的,同一個(gè)符號引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。如果有了直接引用,那么引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
解析動(dòng)作主要針對類或接口、字段、靜態(tài)方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符這幾類符號引用進(jìn)行。
初始化
初始化階段是類加載過程的最后一步。在前面的類加載過程中,除了加載階段我們可以通過自定義的類加載器參與之外,其余的階段都是虛擬機(jī)自動(dòng)完成的。到了初始化階段,才真正開始執(zhí)行我們程序中定義的Java代碼。初始化階段的主要工作是給類的靜態(tài)變量賦予我們程序中指定的初始值。也就是上面準(zhǔn)備階段提到的,變量a的值從0變?yōu)?的過程。這個(gè)階段我們程序指定初始值包括兩種手段:
1、聲明靜態(tài)變量時(shí)顯式的復(fù)制。例如:public static int a = 1; 在初始化階段會(huì)把1賦給變量a。
2、通過靜態(tài)代碼塊賦值。例如:static { a = 2 }; 變量a 的初始值賦為2。
這兩種方式的賦值順序是由語句在源文件中出現(xiàn)的順序來決定的。
以上就是Java虛擬機(jī)類加載機(jī)制的整個(gè)過程以及在每個(gè)階段虛擬機(jī)所執(zhí)行的動(dòng)作。
雙親委派模型
前面提到過,在類加載的整個(gè)過程中,除了加載階段我們可以通過自定義的類加載器參與之外,其他的階段都是虛擬機(jī)幫我們完成的。虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把加載這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”。這樣做的目的是讓應(yīng)用程序自己去決定如何獲取所需要的類。
除了我們自己可以定義類加載器,Java虛擬機(jī)也為我們提供了系統(tǒng)自帶的類加載器。主要可以分為以下三種:
根類加載器(Bootstrap ClassLoader):這個(gè)類加載器負(fù)責(zé)加載存放在<JAVA_HOME>\lib目錄中的,或者通過參數(shù)-Xbootclasspath所指定的路徑中的類。
擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。
應(yīng)用類加載器(Application ClassLoader):它負(fù)責(zé)加載用戶設(shè)置的ClassPath路徑上所指定的類庫。如果應(yīng)用程序中沒有自定義的類加載器,一般情況下這個(gè)就是程序默認(rèn)的類加載器。
我們的應(yīng)用程序都是由這3種類加載器相互配合進(jìn)行加載的,如果有必要,還可以定義自己的類加載器。這些類加載器之間的關(guān)系如下:
上圖中展示的類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型。雙親委派模型要求除了頂層的根類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。雙親委派模型的工作過程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的類加載請求最終都應(yīng)該傳送到根類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
類加載器雖然只用于實(shí)現(xiàn)類的加載動(dòng)作,但是它在Java程序中起的作用卻不僅僅是進(jìn)行類加載。對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立它在Java虛擬機(jī)中的唯一性。簡單來說就是,一個(gè)類的class文件被不同的兩個(gè)類加載器加載,那么加載后的這兩個(gè)類就不“相等”,不是相同的類。
使用雙親委派模型,有一個(gè)顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如java.lang.Object類,它存放在rt.jar中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都會(huì)委派給模型最頂端的根類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類(始終被根類加載器加載)。相反,如果不使用雙親委派模型,由各個(gè)類加載器自己去加載的話,假如用戶編寫了一個(gè)稱為java.lang.Object的類,并放在ClassPath中,那系統(tǒng)中會(huì)出現(xiàn)多個(gè)不同的Object類,應(yīng)用程序也會(huì)變的一片混亂。