Java運(yùn)行時(shí)數(shù)據(jù)區(qū)

Java內(nèi)存模型

Java內(nèi)存模型,就是Java程序運(yùn)行時(shí)的內(nèi)存模型。而Java代碼是在Java虛擬機(jī)上運(yùn)行的,由Java虛擬機(jī)解釋執(zhí)行(解釋器)或者編譯執(zhí)行(即使編譯器,JIT)來完成。所以Java內(nèi)存模型也就是Java虛擬機(jī)的運(yùn)行時(shí)內(nèi)存模型。

Java解釋器,可以在任何移植了解釋器的機(jī)器上執(zhí)行Java字節(jié)碼(.class)。即使編譯器,Java程序運(yùn)行時(shí)動(dòng)態(tài)地將字節(jié)碼翻譯成特定CPU的機(jī)器碼。而編譯指的是.java文件javac成.class文件。

Java內(nèi)存模型結(jié)構(gòu)

C/C++程序猿需要時(shí)刻注意內(nèi)存的釋放,而Java全權(quán)交給虛擬機(jī)來處理內(nèi)存。虛擬機(jī)有垃圾回收(GC),會(huì)去自動(dòng)進(jìn)行垃圾回收。那么都是自動(dòng)的,好學(xué)什么?其實(shí)虛擬機(jī)自動(dòng)回收垃圾有一套自己的規(guī)律,并不能根據(jù)程序猿編寫的代碼隨時(shí)GC,所以只能根據(jù)垃圾回收的規(guī)律,合理安排內(nèi)存,這就要求我們必須徹底了解JVM的內(nèi)存管理機(jī)制。

運(yùn)行時(shí)內(nèi)存模型,分為線程私有和共享數(shù)據(jù)區(qū)兩大類。

線程私有

線程私有的數(shù)據(jù)區(qū)包含程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)。

程序計(jì)數(shù)器

作用:當(dāng)JVM解釋字節(jié)碼文件時(shí),存儲(chǔ)當(dāng)前線程所執(zhí)行的字節(jié)碼行號(hào)。所以每一個(gè)線程都有一個(gè)程序計(jì)數(shù)器。

Java程序執(zhí)行時(shí),是依靠Java虛擬機(jī)的解釋器通過改變當(dāng)前線程程序計(jì)數(shù)器的值來按照流程執(zhí)行指令。當(dāng)然還有一個(gè)好處體現(xiàn)在多線程方面。一個(gè)內(nèi)核同一時(shí)間只能執(zhí)行一條指令,多線程的工作是依靠線程輪流切換(占用CPU時(shí)間)來達(dá)到的。對(duì)于每一個(gè)線程來說,必須有一個(gè)地方存儲(chǔ)中斷時(shí)的地址(行號(hào)),才能保證切回來時(shí)回到正確地址。Java規(guī)范沒有規(guī)定該內(nèi)存區(qū)域OOM(OutOfMemoryError)異常。

注意,上面說的指令指的是Java方法,執(zhí)行native方法時(shí),程序計(jì)數(shù)器為空。

Java虛擬機(jī)棧

虛擬機(jī)棧也是線程私有的,其生命周期和線程一樣。

虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法(不包括native方法)在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀,用于存儲(chǔ)局部變量,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等信息。每一個(gè)方法從調(diào)用到執(zhí)行結(jié)束的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。

在Java虛擬機(jī)規(guī)范中,對(duì)該區(qū)域內(nèi)存規(guī)定了兩種異常狀況:

  • StackOverFlowError:當(dāng)線程請(qǐng)求棧深度超出虛擬機(jī)棧所允許的深度時(shí)拋出
  • OutOfMemoryError:當(dāng)Java虛擬機(jī)動(dòng)態(tài)擴(kuò)展到無法申請(qǐng)足夠內(nèi)存時(shí)拋出
棧幀

棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。

棧幀的概念結(jié)構(gòu).png

在編譯代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到了方法表的Code屬性中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體虛擬機(jī)的實(shí)現(xiàn)。

對(duì)于執(zhí)行引擎來講,活動(dòng)線程中,只有虛擬機(jī)棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀(Current Stack Frame),這個(gè)棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)。執(zhí)行引用所運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。

棧幀包括以下內(nèi)容:

  1. 局部變量表,是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。其大小以slot為最小單位(32位)。在Java程序編譯為Class文件時(shí),就在方法表的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法需要分配的最大局部變量表的容量。它存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean,byte,char,short,int,float,long,double),對(duì)象引用,以及returnAddress類型(指向了一條字節(jié)碼指令地址)
  2. 操作數(shù)棧,操作數(shù)棧也常被稱為操作棧,它是一個(gè)后入先出棧。同局部變量表一樣,操作數(shù)棧的最大深度也是編譯的時(shí)候被寫入到方法表的Code屬性的max_stacks數(shù)據(jù)項(xiàng)中。
  3. 動(dòng)態(tài)鏈接,每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬性方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接。在Class文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運(yùn)行期期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。
  4. 方法返回地址,當(dāng)一個(gè)方法被執(zhí)行后,有兩種方式退出。
    • 執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的的方法稱為調(diào)用者)。是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法方式稱為正常完成出口(Normal Method Invocation Completion)。
    • 在方法執(zhí)行過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理。只要該異常在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的調(diào)用都產(chǎn)生任何返回值的。
  5. 附加信息,虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中,例如與高度相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)。在實(shí)際開發(fā)中,一般會(huì)把動(dòng)態(tài)連接,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息。

執(zhí)行引擎,Java虛擬機(jī)的執(zhí)行引擎在執(zhí)行代碼時(shí),都有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過JIT產(chǎn)生本地代碼(特定CPU機(jī)器碼)執(zhí)行)兩種選擇。

以上內(nèi)容參考自深入理解Java虛擬機(jī)筆記---運(yùn)行時(shí)棧幀結(jié)構(gòu),只是做到了了解棧幀的結(jié)構(gòu)。

日常開發(fā)中,經(jīng)常把內(nèi)存模型分為堆和棧,而其中的棧其實(shí)就是虛擬機(jī)棧,更準(zhǔn)確的說是虛擬機(jī)棧中棧幀的局部變量表。

本地方法棧

本地方法棧則為虛擬機(jī)使用到的Native方法提供內(nèi)存空間。有些虛擬機(jī)的實(shí)現(xiàn)直接把本地方法棧和虛擬機(jī)棧合二為一,比如非常典型的Sun HotSpot虛擬機(jī)。

和虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常。

共享數(shù)據(jù)區(qū)

所有線程共享的數(shù)據(jù)區(qū)包含Java堆、方法區(qū),在方法區(qū)內(nèi)有一個(gè)常量池。

堆內(nèi)存

Java堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存唯一的目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存(隨著JIT的發(fā)展,有一部分對(duì)象實(shí)例會(huì)放在棧內(nèi)存)。

Java堆事垃圾收集器管理的主要區(qū)域,又稱其為"GC堆"(Garbage Collection Heap)。

  • 從內(nèi)存回收的角度來看,Java堆可以細(xì)分為新生代和老年代(舊生代)。
  • 從內(nèi)存分配的角度來看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)。

進(jìn)一步劃分的目的是為了更好的回收內(nèi)存。

在32位系統(tǒng)上最大為2G,64位系統(tǒng)上無限制??赏ㄟ^-Xms和-Xmx控制,-Xms為JVM啟動(dòng)時(shí)申請(qǐng)的最小Heap內(nèi)存,-Xmx為JVM可申請(qǐng)的最大Heap內(nèi)存。

如果堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。

方法區(qū)

它主要存放一杯虛擬機(jī)加載的類信息,常量,靜態(tài)變量,即使編譯器后的代碼等數(shù)據(jù)。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。

運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量區(qū)是方法區(qū)的一部分。用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。還會(huì)有一些符號(hào)引用轉(zhuǎn)換的直接引用一保存在運(yùn)行時(shí)常量池中。

運(yùn)行時(shí)常量池具備動(dòng)態(tài)性,也就是運(yùn)行期間也可以將新的常量放入池中,例如String.intern()方法。

當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí),會(huì)拋出OutOfMemoryError異常。

直接內(nèi)存

上面描述的內(nèi)存模型是運(yùn)行時(shí)的數(shù)據(jù)區(qū)。而直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。這部分區(qū)域也會(huì)導(dǎo)致OutOfMemoryError異常出現(xiàn)。

在JDK1.4中新加入類NIO類,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存。它可以使用Native函數(shù)庫直接分配堆外內(nèi)存。

由于它是本機(jī)內(nèi)存(Ram),肯定會(huì)受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制。所以在動(dòng)態(tài)擴(kuò)展時(shí)會(huì)出現(xiàn)OOM異常。

雜記

變量的初始化

變量根據(jù)作用域分有局部變量和類變量。局部變量不像類變量那樣存在“準(zhǔn)備階段”。類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另外一次在初始化階段,賦予程序員定義的值。因此即使在初始化階段程序員沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個(gè)確定的初始值。但局部變量就不一樣了,如果一個(gè)局部變量定義了但沒有賦初始值是不能使用的。

字面量,常量,直接量,符號(hào)引用類型

  • 字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明為final的常量值等。還包含了基本數(shù)據(jù)類型的直接量,以及引用類型的空指向。
  • 符號(hào)引用:編譯語言層面的概念,包括以下3類:
    • 類和接口的全限定名
    • 字段的名稱和描述符
    • 方法的名稱和描述符

直接量,指在程序中直接出線的常量值。

遺留問題

遺留問題.png

只有程序計(jì)數(shù)器內(nèi)存區(qū)域不會(huì)拋出OOM異常,其它能夠拋出OOM異常的內(nèi)存區(qū)域,都有動(dòng)態(tài)擴(kuò)展這一特性。

參考

Jvm內(nèi)存模型

Java之美[從菜鳥到高手演變]之JVM內(nèi)存管理及垃圾回收

深入理解Java虛擬機(jī)筆記---運(yùn)行時(shí)棧幀結(jié)構(gòu)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容