Java內(nèi)存區(qū)域
1. 運(yùn)行時(shí)數(shù)據(jù)區(qū)域
在上一篇博客中提到了虛擬機(jī)的運(yùn)行的時(shí)候,需要加載類,以及存儲(chǔ)數(shù)據(jù)等,因此需要有個(gè)區(qū)域用來存儲(chǔ)運(yùn)行時(shí)的數(shù)據(jù)。
上一篇博客也提到了JVM的體系結(jié)構(gòu),可以看出這幅圖中的運(yùn)行時(shí)數(shù)據(jù)區(qū)的劃分是和JVM體系結(jié)構(gòu)相關(guān)的。
Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)把自己所管理的內(nèi)存區(qū)域劃分成若干個(gè)數(shù)據(jù)區(qū)域嗎,這些區(qū)域都有各自的用途以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域則依賴用戶的線程的啟動(dòng)和結(jié)束而建立和銷毀。
1.PC寄存器
PC寄存器,也就是我們常說的程序計(jì)數(shù)器,是一塊較小的內(nèi)存空間,用于存放一條指令的地址, 這條指令便是虛擬機(jī)要執(zhí)行的下一條指令。它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型中,字節(jié)碼解釋器就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條要執(zhí)行的指令。
每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,因?yàn)槎嗑€程是通過線程輪流切換并分配時(shí)間處理執(zhí)行的方式實(shí)現(xiàn)的,因此為了保證線程切換之后,能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,且各個(gè)線程的程序計(jì)數(shù)器互不影響。這類內(nèi)存區(qū)域稱為線程私有的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè)Java方法,則這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的指令地址,如果是Native方法,這個(gè)計(jì)數(shù)器的值就為空,此區(qū)域是唯一一個(gè)Java虛擬機(jī)規(guī)范沒有規(guī)定任何OOM情況的區(qū)域。
2.Java棧
與PC寄存器一樣,Java棧也是線程私有的,它的生命周期與線程相同,每個(gè)方法的執(zhí)行和結(jié)束都對(duì)應(yīng)著一個(gè)棧幀(棧的基本單位)的入棧和出棧。每個(gè)方法對(duì)應(yīng)一個(gè)棧幀,用于存儲(chǔ)局部表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等。
局部變量表的內(nèi)存空間是在編譯期間完成分配的,當(dāng)進(jìn)入一個(gè)方法時(shí),整個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的。
在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常情況,一個(gè)是StackOverflowError異常,如果一個(gè)線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將會(huì)拋出這個(gè)異常。一個(gè)是OOM異常,對(duì)于可以動(dòng)態(tài)擴(kuò)展的虛擬機(jī)棧,如果擴(kuò)展無法申請(qǐng)到足夠的內(nèi)存,便會(huì)報(bào)出此異常。
3.本地方法棧
本地方法棧是為Native方法服務(wù)的,而Java棧是為Java方法服務(wù)的,當(dāng)Java調(diào)用C/C++等Native方法時(shí), 便會(huì)使用本地方法棧,它也是線程私有的。
4.Java堆
Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)被啟動(dòng)時(shí)創(chuàng)建,此內(nèi)存區(qū)域的唯一目的便是存放對(duì)象實(shí)例以及數(shù)組,在java中數(shù)組也是對(duì)象。虛擬機(jī)規(guī)范的描述是:所有的對(duì)象實(shí)例和數(shù)組都要在堆上分配。(實(shí)例變量是存儲(chǔ)在堆上的對(duì)象中的,一個(gè)對(duì)象有一份實(shí)例變量)
隨著逃逸分析和TLAB技術(shù)的成熟,便出現(xiàn)了對(duì)象實(shí)例不一定在堆上分配空間的情況。在此介紹下這兩個(gè)技術(shù)。
逃逸技術(shù)
逃逸分析,是一種可以有效減少Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析[算法。通過逃逸分析,Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。
逃逸分析是指分析指針動(dòng)態(tài)范圍的方法,當(dāng)變量(或者對(duì)象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會(huì)被其他過程或者線程所引用,這種現(xiàn)象稱作指針(或者引用)的逃逸(Escape)。
對(duì)象的三種逃逸狀態(tài)
GlobalEscape(全局逃逸):即一個(gè)對(duì)象的引用逃出了方法和線程,如:一個(gè)對(duì)象的引用是賦值給類變量,或者作為返回值返回給調(diào)用方法。
ArgEscape(參數(shù)級(jí)逃逸):即在方法調(diào)用過程中,將對(duì)象的引用作為參數(shù)傳遞給方法。
NoEscape(沒有逃逸):只是在方法中new出來,在方法中使用,可以不將這種對(duì)象分配在傳統(tǒng)的堆上,而是分配在棧上。
public void test(){
A a = new A();
B b = new B();
A.XXX(b);
}
上面是一個(gè)簡單的實(shí)例,按照我們以往的想法,無非是對(duì)象的引用a,b分配在棧中,而new出來的A,B對(duì)象是存儲(chǔ)在堆中的,而使用了逃逸技術(shù),顯然new出的A對(duì)象是屬于沒有逃逸的,而new出的B對(duì)象雖然作為參數(shù)傳遞給了A的方法,然而A是沒有逃逸的,因此這兩個(gè)對(duì)象都是屬于NoEscape,可分配在棧中。
逃逸技術(shù)還有其他應(yīng)用,如
清除同步:線程同步的代價(jià)是降低了并發(fā)性和性能。逃逸分析可以判斷某個(gè)對(duì)象是否始終被一個(gè)線程訪問,若是的話,就可以撤銷對(duì)該對(duì)象的同步保護(hù),從而提高并發(fā)程度和性能。
矢量替代:逃逸分析方法如果發(fā)現(xiàn)對(duì)象的內(nèi)存存儲(chǔ)結(jié)構(gòu)不需要連續(xù)進(jìn)行的話,就可以將對(duì)象的部分甚至全部都保存在CPU寄存器內(nèi),這樣能大大提高訪問速度。
TALB
JVM在內(nèi)存新生代Eden Space中開辟了一小塊線程私有的區(qū)域,稱作TLAB(Thread-local allocation buffer)。在Java程序中很多對(duì)象都是小對(duì)象且用過即丟,它們不存在線程共享也適合被快速GC,所以對(duì)于小對(duì)象通常JVM會(huì)優(yōu)先分配在TLAB上,并且TLAB上的分配由于是線程私有所以沒有鎖開銷。
總結(jié)
對(duì)象不在堆上分配主要的原因是堆是共享的,在堆上分配對(duì)象有鎖的開銷,且堆上的分配的對(duì)象內(nèi)存需要GC才能進(jìn)行回收。
5.方法區(qū)
方法區(qū)和Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域。它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量,即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)是一個(gè)相對(duì)穩(wěn)定的內(nèi)存區(qū), 因?yàn)樗娣诺氖穷愋托畔ⅲ?而類型信息在被加載到方法區(qū)中之后, 除了必要的連接和初始化, 一般不會(huì)有較大改動(dòng) 。一般情況下, JVM也不會(huì)卸載類型信息, 所以方法區(qū)也可以稱為JVM的靜態(tài)區(qū)。 一個(gè)類型的生命周期一般就是整個(gè)程序的生命周期。 一個(gè)JVM實(shí)例中只存在一個(gè)方法區(qū), 方法區(qū)中的所有類型數(shù)據(jù)被所有線程共享。
6.運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class文件中除了有類的版本,字段,方法等,還有一項(xiàng)信息就是常量池,用于存放編譯期生成的各種字面量和符號(hào)引用,而這些內(nèi)容將會(huì)在類加載之后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量。同時(shí),運(yùn)行期間也能將新的常量放入池中。
歡迎關(guān)注本人博客:https://allen-yu.com/