JVM研習之一:Java虛擬機的內(nèi)存結(jié)構(gòu)

注1:以下所提及線程,無特定說明的均默認指代“Java虛擬機線程”。

注2:注意避免混淆Stack、Heap和Java(VM) Stack、Java Heap的概念。Java虛擬機是在操作系統(tǒng)之上的更高層抽象,可以看作是一臺虛擬的計算機。Java虛擬機的內(nèi)存劃分與操作系統(tǒng)的內(nèi)存分區(qū)無法一一對應(yīng)。Java虛擬機的實現(xiàn)本身是由其他語言編寫的應(yīng)用程序,在Java語言程序的角度上看分配在Java Stack中的數(shù)據(jù),而在實現(xiàn)虛擬機的程序角度上看則可以是分配在Heap之中。

Java虛擬機的內(nèi)存結(jié)構(gòu),區(qū)別于側(cè)重于多線程的Java內(nèi)存模型(Java?Memory?Model)。

我們當然要精確地定義每個區(qū)域的概念和用途,但在此之前(或之后),是不是應(yīng)該思考一下:JVM的內(nèi)存結(jié)構(gòu)為什么要這樣劃分?

私以為主要是依據(jù)不同數(shù)據(jù)的更新頻率、訪問速度要求、垃圾收集管理來劃分的。線程的工作內(nèi)存區(qū),要求速度快,也受限于高速讀寫設(shè)備資源的稀缺性,JVM據(jù)此劃分了PC寄存器、Stack(棧),用來存儲當前方法操作計數(shù)和局部變量(基本類型及引用)。對于更新頻率較低的類結(jié)構(gòu)信息、常量、靜態(tài)方法等,劃分了“方法區(qū)”。對于占用容量較大的對象實例,劃分了“Java Heap(堆)”。為了區(qū)分Java方法和Native方法的處理,又將 Stack拆分為JVM?Stack(Java虛擬機棧區(qū))、Native Method Stack(本地方法棧)。
這就是JVM的五大內(nèi)存區(qū)了。

1、PC寄存器,線程私有,小,快;
2、JVM?Stack,線程私有,小,快;
3、Native?Method?Stack,線程私有,小,快;
4、Java?Heap,共享區(qū),大,容量動態(tài)分配,慢;
5、方法區(qū),共享區(qū),中;

名稱中英對照

* 運行時數(shù)據(jù)區(qū) - (Run-Time Data Areas)
? ? - PC寄存器 - (Program Counter Register)
? ? - Java虛擬機棧 - (JVM Stack)
? ? ? * 棧幀 - (Frame)
? ? ? ? * 局部變量表 - (Local Variables)
? ? ? ? * 操作數(shù)棧 - (Operand Stack)
? ? ? ? * 動態(tài)鏈接 - (Dynamic Linking)
? ? - Java堆 - (Java Heap)
? ? - 方法區(qū)? - (Method Area)
? ? ? * 運行時常量池? - (Runtime Constant Pool)
? ? - 本地方法棧? - (Native Method Stack)


各區(qū)的定義(JVM規(guī)范)

JVM定義了幾種程序運行期間會使用到的運行時數(shù)據(jù)區(qū),分別對應(yīng)JVM或線程的生命周期。

PC寄存器:每一條線程都有自己的PC寄存器。正在被線程執(zhí)行的current method,如其不是native的,PC寄存器就保存JVM正在執(zhí)行的字節(jié)碼指令的地址,如其是native的,則值為undefined。

JVM Stack:每一條線程都有自己私有的JVM Stack。其與線程同時創(chuàng)建,用于存儲Frame。與傳統(tǒng)語言(C)中的棧類似,JVM Stack用于存儲局部變量與一些過程結(jié)果。它在方法調(diào)用和返回中也扮演了很重要的角色。因為除了Frame的出棧和入棧外,JVM Stack不會再受其他因素影響,所以Frame可以在堆中分配。

本地方法棧:JVM實現(xiàn)可能會使用到傳統(tǒng)的棧(C Stack)來支持native方法的執(zhí)行,這個棧就是本地方法棧。當JVM使用其他語言來實現(xiàn)指令集解釋器時,也會使用本地方法棧。這個棧一般在線程創(chuàng)建時按線程分配。

Java Heap?:在JVM中,Heap是可供各條線程共享的運行時內(nèi)存區(qū),也是供所有類實例和數(shù)據(jù)對象分配內(nèi)存的區(qū)域。Heap在JVM啟動的時候被創(chuàng)建,它存儲了被ASMS/GC所管理的各種對象,這些對象無需也無法顯式地被銷毀。

方法區(qū):在JVM中,方法區(qū)是可供各條線程共享的運行時內(nèi)存區(qū)域。它存儲了每一個類的結(jié)構(gòu)信息,例如運行時常量池、字段和方法數(shù)據(jù)、構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容。方法區(qū)在虛擬機啟動的時候被創(chuàng)建。

運行時常量池:該池是每一個類或接口的常量池的運行時表示形式,它包括了若干種不同的常量:從編譯期可知的數(shù)值字面值到必須運行期解析才能獲得的方法或字段引用。運行時常量池扮演了類似傳統(tǒng)語言中符號表(Symbol Table)的角色,不過它存儲數(shù)據(jù)范圍比通常意義上的符號表要更為廣泛。每一個運行時常量池都分配在Java虛擬機的方法區(qū)之中,在類和接口被加載到虛擬機后,對應(yīng)的的運行時常量池就被創(chuàng)建出來。

棧幀:Frame是用來存儲數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時也被用來處理動態(tài)鏈接、方法返回值和異常分派。Frame隨方法調(diào)用而創(chuàng)建,隨方法結(jié)束而銷毀——無論是正常完成或是異常完成(拋出了在方法內(nèi)無未被捕獲的異常)都算作方法結(jié)束。Frame在存儲空間分配在JVM Stack之中,每一個Frame都有自己的局部變量表、操作數(shù)棧和指向當前方法所屬的類的運行時常量池的引用。

局部變量表和操作數(shù)棧的容量是在編譯期確定,并通過方法的Code屬性保存和提供給Frame使用。因此,F(xiàn)rame容量的大小僅僅取決于JVM的實現(xiàn)和方法調(diào)用時可被分配的內(nèi)存。

在一條線程中,只有當前正在執(zhí)行的那個方法的Frame是活動的,稱為Current Frame(當前棧幀,簡稱CF),其對應(yīng)的方法稱為Current Method(簡稱CM),定義該方法的類就稱作Current Class。對局部變量表和操作數(shù)棧的各種操作,通常都指的是對當前棧幀的對局部變量表和操作數(shù)棧進行的操作。

如果CM調(diào)用了其他方法,或者CM執(zhí)行結(jié)束,那這個方法的Frame就不再是CF了。當一個新的方法被調(diào)用,一個新的Frame也會隨之而創(chuàng)建,并且隨著程序控制權(quán)移交到新的方法而成為新的CF。當方法返回之際,CF會傳回方法的執(zhí)行結(jié)果給前一個Frame,在方法返回之后,CF隨之被丟棄,前一個Frame就重新成為CF了。

需要特別注意的是,F(xiàn)rame是線程本地私有的數(shù)據(jù),不同線程的Frame不能被互相訪問或引用。

局部變量表:每個Frame內(nèi)部都包含一組稱為“局部變量表”的變量列表。它的長度由編譯期決定,并且存儲于類和接口的二進制表示之中,即通過方法的Code屬性保存及提供給Frame使用。本區(qū)保存8大基本類型以及Reference、returnAddress類型的數(shù)據(jù),其中double和long類型的數(shù)據(jù)占兩個變量位,其余的占一個。JVM使用局部變量表來完成方法調(diào)用時的參數(shù)傳遞,當一個方法被調(diào)用的時候,它的參數(shù)將傳遞至從0開始的連續(xù)的局部變量表位置上。特別地,當一個實例方法被調(diào)用的時候,第0個局部變量一定是用來存儲被調(diào)用的實例方法所在的對象的引用(即this關(guān)鍵字)。后續(xù)的其他參數(shù)將會傳遞至從1開始的連續(xù)的局部變量表位置上。

操作數(shù)棧:每個Frame內(nèi)部都包含一個稱為“操作數(shù)?!钡暮筮M先出棧(LIFO)。它的長度由編譯期決定,并且存儲于類和接口的二進制表示中,即通過方法的Code屬性保存及提供給Frame使用。在上下文明確,不會產(chǎn)生誤解的前提下,我們經(jīng)常把“當前棧幀的操作數(shù)?!敝苯雍喎Q為“操作數(shù)?!?。

操作數(shù)棧所屬的Frame在創(chuàng)建初時,操作數(shù)棧是空的。JVM提供一些字節(jié)碼指令來從局部變量表或?qū)ο髮嵗淖侄沃袕?fù)制常量或變量值到操作數(shù)棧中,也提供了一些指令用于從操作數(shù)棧中取走數(shù)據(jù)、操作數(shù)據(jù)和把操作結(jié)果重新入棧。在方法調(diào)用時,操作數(shù)棧也用來準備調(diào)用方法的參數(shù)以及接收方法返回結(jié)果。(過程細節(jié)見$2.6.2)

動態(tài)鏈接:每個Frame內(nèi)部都包含一個指向運行時常量池的引用來支持當前方法的代碼實現(xiàn)動態(tài)鏈接。

各區(qū)的深入理解

1.PC寄存器

PC寄存器(Program Counter Register,程序計數(shù)器),是一塊較小的內(nèi)存空間,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機的概念模型里,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。

由于JVM的多線程是通過輪流切換分配CPU執(zhí)行時間的方式來實現(xiàn)的,在某個特定時刻,一個CPU/內(nèi)核只會執(zhí)行一條線程的指令。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個私有、獨立的PC寄存器,各線程間互不影響,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。

如果線程當前執(zhí)行的是一個Java方法,PC寄存器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;

如果線程正在執(zhí)行的是native方法,它的值則為空(undefined)。

2.JVM Stack

和PC寄存器一樣,JVM Stack也是線程私有的。它的生命周期與線程相同。JVM Stack描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個Frame用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個Frame在JVM Stack中入棧到出棧的過程。

經(jīng)常有人把Java內(nèi)存分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種分法比較粗糙,Java內(nèi)存區(qū)域的劃分遠比這復(fù)雜。其中所指“?!本褪乾F(xiàn)在講的JVM Stack,或者說是JVM Stack中局部變量表部分(直接越過了Stack和Frame)。

局部變量表,存放了編譯期可知的8大基本數(shù)據(jù)類型、對象引用(reference類型,并不是對象本身,可能只是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。局部變量表所需的空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

當線程請求分配的棧容量超過JVM允許的最大容量時,將拋出StackOverflowError;

如果JVM可動態(tài)擴展,并且擴展的動作已經(jīng)嘗試過,但是目前無法申請到足夠的內(nèi)存去完成擴展,或在建立新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的JVM Stack,將拋出OutOfMemoryError。

StackOverflowError 表示 請求 > Stack.Max;

OutOfMemoryError 表示 請求 > 可分配內(nèi)存;

3.本地方法棧

本地方法棧與JVM Stack所發(fā)揮的作用是非常相似的,區(qū)別只是JVM Stack為虛擬機執(zhí)行Java方法服務(wù),而本地方法棧是為虛擬機使用到的Native方法服務(wù)。

在虛擬機規(guī)范中對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)沒有強制規(guī)定,由實現(xiàn)自由選擇。甚至有的虛擬機(如HotSpot)直接將本地方法棧和JVM Stack合二為一。與JVM Stack一樣,也會拋出StackOverflowError和OutOfMemoryError異常。

4.Java Heap

對大多數(shù)應(yīng)用來說,Java堆(Java Heap)是JVM所管理的內(nèi)存中最大的一塊,它是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。該區(qū)的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。

Java Heap是GC管理的主要區(qū)域。由于現(xiàn)在收集器基本都采用了分代收集算法,所以Java Heap還細分為:新生代和老年代;再細一點的 Eden空間、From Survivor空間、To Survivor空間等。不過無論怎么劃分,都與存放內(nèi)容無關(guān),無論哪個區(qū)域存儲的都是對象實例。

在實現(xiàn)時,既可以是固定大小的,也可以是擴展的,不過主流的虛擬機都是按照可擴展來實現(xiàn)的(-Xmx和-Xms)。如果堆 中沒有內(nèi)存完成實例分配,并且堆無法再擴展時,就會拋出OutOfMemoryError。

5.方法區(qū)

與Java Heap一樣,方法區(qū)是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。雖然JVM規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫Non-Heap(非堆),目的應(yīng)該是與Java Heap分區(qū)來的。

Java虛擬機規(guī)范對方法區(qū)的限制非常寬松,可以選擇不實現(xiàn)垃圾收集。垃圾收集行為在本區(qū)是比較少出現(xiàn)的,但非數(shù)據(jù)進入方法區(qū)就如永久代的名字一樣“永久”存在了。這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載,一般來說,這個區(qū)域的回收“成績”很難令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區(qū)域的回收確實是有必要的。

運行時常量池

運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。

Java虛擬機對Class文件每一部分(自然也包括常量池)的格式都有嚴格規(guī)定,每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會被虛擬機認可、裝載和執(zhí)行,但對于運行時常量池,Java虛擬機規(guī)范沒有做任何細節(jié)的要求,不同的提供商實現(xiàn)的虛擬機可以按照自己的需要來實現(xiàn)這個內(nèi)存區(qū)域。

既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當常量池無法再申請到內(nèi)存時拋出OutOfMemoryError異常。

各區(qū)容量的規(guī)范要求

PC寄存器:容量至少應(yīng)當能保存一個returnAddress類型的數(shù)據(jù)或一個與平臺相關(guān)的本地指針的值。(不可調(diào)節(jié))
JVM Stack:可調(diào)節(jié)初始容量,對于可動態(tài)擴展和收縮的,可調(diào)節(jié)最大、最小容量。
Java Heap:可調(diào)節(jié)初始容量,對于可動態(tài)擴展和收縮的,可調(diào)節(jié)最大、最小容量。
方法區(qū):可調(diào)節(jié)初始容量,對于可動態(tài)擴展和收縮的,可調(diào)節(jié)最大、最小容量。
本地方法棧:可調(diào)節(jié)初始容量,對于可動態(tài)擴展和收縮的,可調(diào)節(jié)最大、最小容量。

參考

1、《Java虛擬機規(guī)范SE7》第2章 Java虛擬機結(jié)構(gòu)
2、《深入理解Java虛擬機》第2章 Java內(nèi)存區(qū)域

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

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