注1:以下所提及線程,無特定說明的均默認指代“Java虛擬機線程”。
注2:注意避免混淆Stack、Heap和Java(VM) Stack、Java Heap的概念。Java虛擬機是在操作系統之上的更高層抽象,可以看作是一臺虛擬的計算機。Java虛擬機的內存劃分與操作系統的內存分區無法一一對應。Java虛擬機的實現本身是由其他語言編寫的應用程序,在Java語言程序的角度上看分配在Java Stack中的數據,而在實現虛擬機的程序角度上看則可以是分配在Heap之中。
Java虛擬機的內存結構,區別于側重于多線程的Java內存模型(Java?Memory?Model)。
我們當然要精確地定義每個區域的概念和用途,但在此之前(或之后),是不是應該思考一下:JVM的內存結構為什么要這樣劃分?
私以為主要是依據不同數據的更新頻率、訪問速度要求、垃圾收集管理來劃分的。線程的工作內存區,要求速度快,也受限于高速讀寫設備資源的稀缺性,JVM據此劃分了PC寄存器、Stack(棧),用來存儲當前方法操作計數和局部變量(基本類型及引用)。對于更新頻率較低的類結構信息、常量、靜態方法等,劃分了“方法區”。對于占用容量較大的對象實例,劃分了“Java Heap(堆)”。為了區分Java方法和Native方法的處理,又將 Stack拆分為JVM?Stack(Java虛擬機棧區)、Native Method Stack(本地方法棧)。
這就是JVM的五大內存區了。
1、PC寄存器,線程私有,小,快;
2、JVM?Stack,線程私有,小,快;
3、Native?Method?Stack,線程私有,小,快;
4、Java?Heap,共享區,大,容量動態分配,慢;
5、方法區,共享區,中;
名稱中英對照
* 運行時數據區 - (Run-Time Data Areas)
? ? - PC寄存器 - (Program Counter Register)
? ? - Java虛擬機棧 - (JVM Stack)
? ? ? * 棧幀 - (Frame)
? ? ? ? * 局部變量表 - (Local Variables)
? ? ? ? * 操作數棧 - (Operand Stack)
? ? ? ? * 動態鏈接 - (Dynamic Linking)
? ? - Java堆 - (Java Heap)
? ? - 方法區? - (Method Area)
? ? ? * 運行時常量池? - (Runtime Constant Pool)
? ? - 本地方法棧? - (Native Method Stack)
各區的定義(JVM規范)
JVM定義了幾種程序運行期間會使用到的運行時數據區,分別對應JVM或線程的生命周期。
PC寄存器:每一條線程都有自己的PC寄存器。正在被線程執行的current method,如其不是native的,PC寄存器就保存JVM正在執行的字節碼指令的地址,如其是native的,則值為undefined。
JVM Stack:每一條線程都有自己私有的JVM Stack。其與線程同時創建,用于存儲Frame。與傳統語言(C)中的棧類似,JVM Stack用于存儲局部變量與一些過程結果。它在方法調用和返回中也扮演了很重要的角色。因為除了Frame的出棧和入棧外,JVM Stack不會再受其他因素影響,所以Frame可以在堆中分配。
本地方法棧:JVM實現可能會使用到傳統的棧(C Stack)來支持native方法的執行,這個棧就是本地方法棧。當JVM使用其他語言來實現指令集解釋器時,也會使用本地方法棧。這個棧一般在線程創建時按線程分配。
Java Heap?:在JVM中,Heap是可供各條線程共享的運行時內存區,也是供所有類實例和數據對象分配內存的區域。Heap在JVM啟動的時候被創建,它存儲了被ASMS/GC所管理的各種對象,這些對象無需也無法顯式地被銷毀。
方法區:在JVM中,方法區是可供各條線程共享的運行時內存區域。它存儲了每一個類的結構信息,例如運行時常量池、字段和方法數據、構造函數和普通方法的字節碼內容。方法區在虛擬機啟動的時候被創建。
運行時常量池:該池是每一個類或接口的常量池的運行時表示形式,它包括了若干種不同的常量:從編譯期可知的數值字面值到必須運行期解析才能獲得的方法或字段引用。運行時常量池扮演了類似傳統語言中符號表(Symbol Table)的角色,不過它存儲數據范圍比通常意義上的符號表要更為廣泛。每一個運行時常量池都分配在Java虛擬機的方法區之中,在類和接口被加載到虛擬機后,對應的的運行時常量池就被創建出來。
棧幀:Frame是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接、方法返回值和異常分派。Frame隨方法調用而創建,隨方法結束而銷毀——無論是正常完成或是異常完成(拋出了在方法內無未被捕獲的異常)都算作方法結束。Frame在存儲空間分配在JVM Stack之中,每一個Frame都有自己的局部變量表、操作數棧和指向當前方法所屬的類的運行時常量池的引用。
局部變量表和操作數棧的容量是在編譯期確定,并通過方法的Code屬性保存和提供給Frame使用。因此,Frame容量的大小僅僅取決于JVM的實現和方法調用時可被分配的內存。
在一條線程中,只有當前正在執行的那個方法的Frame是活動的,稱為Current Frame(當前棧幀,簡稱CF),其對應的方法稱為Current Method(簡稱CM),定義該方法的類就稱作Current Class。對局部變量表和操作數棧的各種操作,通常都指的是對當前棧幀的對局部變量表和操作數棧進行的操作。
如果CM調用了其他方法,或者CM執行結束,那這個方法的Frame就不再是CF了。當一個新的方法被調用,一個新的Frame也會隨之而創建,并且隨著程序控制權移交到新的方法而成為新的CF。當方法返回之際,CF會傳回方法的執行結果給前一個Frame,在方法返回之后,CF隨之被丟棄,前一個Frame就重新成為CF了。
需要特別注意的是,Frame是線程本地私有的數據,不同線程的Frame不能被互相訪問或引用。
局部變量表:每個Frame內部都包含一組稱為“局部變量表”的變量列表。它的長度由編譯期決定,并且存儲于類和接口的二進制表示之中,即通過方法的Code屬性保存及提供給Frame使用。本區保存8大基本類型以及Reference、returnAddress類型的數據,其中double和long類型的數據占兩個變量位,其余的占一個。JVM使用局部變量表來完成方法調用時的參數傳遞,當一個方法被調用的時候,它的參數將傳遞至從0開始的連續的局部變量表位置上。特別地,當一個實例方法被調用的時候,第0個局部變量一定是用來存儲被調用的實例方法所在的對象的引用(即this關鍵字)。后續的其他參數將會傳遞至從1開始的連續的局部變量表位置上。
操作數棧:每個Frame內部都包含一個稱為“操作數棧”的后進先出棧(LIFO)。它的長度由編譯期決定,并且存儲于類和接口的二進制表示中,即通過方法的Code屬性保存及提供給Frame使用。在上下文明確,不會產生誤解的前提下,我們經常把“當前棧幀的操作數棧”直接簡稱為“操作數棧”。
操作數棧所屬的Frame在創建初時,操作數棧是空的。JVM提供一些字節碼指令來從局部變量表或對象實例的字段中復制常量或變量值到操作數棧中,也提供了一些指令用于從操作數棧中取走數據、操作數據和把操作結果重新入棧。在方法調用時,操作數棧也用來準備調用方法的參數以及接收方法返回結果。(過程細節見$2.6.2)
動態鏈接:每個Frame內部都包含一個指向運行時常量池的引用來支持當前方法的代碼實現動態鏈接。
各區的深入理解
1.PC寄存器
PC寄存器(Program Counter Register,程序計數器),是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
由于JVM的多線程是通過輪流切換分配CPU執行時間的方式來實現的,在某個特定時刻,一個CPU/內核只會執行一條線程的指令。為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個私有、獨立的PC寄存器,各線程間互不影響,我們稱這類內存區域為“線程私有”的內存。
如果線程當前執行的是一個Java方法,PC寄存器記錄的是正在執行的虛擬機字節碼指令的地址;
如果線程正在執行的是native方法,它的值則為空(undefined)。
2.JVM Stack
和PC寄存器一樣,JVM Stack也是線程私有的。它的生命周期與線程相同。JVM Stack描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個Frame用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用到執行完成的過程,就對應著一個Frame在JVM Stack中入棧到出棧的過程。
經常有人把Java內存分為堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分遠比這復雜。其中所指“棧”就是現在講的JVM Stack,或者說是JVM Stack中局部變量表部分(直接越過了Stack和Frame)。
局部變量表,存放了編譯期可知的8大基本數據類型、對象引用(reference類型,并不是對象本身,可能只是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。局部變量表所需的空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
當線程請求分配的棧容量超過JVM允許的最大容量時,將拋出StackOverflowError;
如果JVM可動態擴展,并且擴展的動作已經嘗試過,但是目前無法申請到足夠的內存去完成擴展,或在建立新的線程時沒有足夠的內存去創建對應的JVM Stack,將拋出OutOfMemoryError。
StackOverflowError 表示 請求 > Stack.Max;
OutOfMemoryError 表示 請求 > 可分配內存;
3.本地方法棧
本地方法棧與JVM Stack所發揮的作用是非常相似的,區別只是JVM Stack為虛擬機執行Java方法服務,而本地方法棧是為虛擬機使用到的Native方法服務。
在虛擬機規范中對本地方法棧中方法使用的語言、使用方式與數據結構沒有強制規定,由實現自由選擇。甚至有的虛擬機(如HotSpot)直接將本地方法棧和JVM Stack合二為一。與JVM Stack一樣,也會拋出StackOverflowError和OutOfMemoryError異常。
4.Java Heap
對大多數應用來說,Java堆(Java Heap)是JVM所管理的內存中最大的一塊,它是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。該區的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。
Java Heap是GC管理的主要區域。由于現在收集器基本都采用了分代收集算法,所以Java Heap還細分為:新生代和老年代;再細一點的 Eden空間、From Survivor空間、To Survivor空間等。不過無論怎么劃分,都與存放內容無關,無論哪個區域存儲的都是對象實例。
在實現時,既可以是固定大小的,也可以是擴展的,不過主流的虛擬機都是按照可擴展來實現的(-Xmx和-Xms)。如果堆 中沒有內存完成實例分配,并且堆無法再擴展時,就會拋出OutOfMemoryError。
5.方法區
與Java Heap一樣,方法區是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然JVM規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫Non-Heap(非堆),目的應該是與Java Heap分區來的。
Java虛擬機規范對方法區的限制非常寬松,可以選擇不實現垃圾收集。垃圾收集行為在本區是比較少出現的,但非數據進入方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說,這個區域的回收“成績”很難令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。
運行時常量池
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
Java虛擬機對Class文件每一部分(自然也包括常量池)的格式都有嚴格規定,每一個字節用于存儲哪種數據都必須符合規范上的要求才會被虛擬機認可、裝載和執行,但對于運行時常量池,Java虛擬機規范沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時拋出OutOfMemoryError異常。
各區容量的規范要求
PC寄存器:容量至少應當能保存一個returnAddress類型的數據或一個與平臺相關的本地指針的值。(不可調節)
JVM Stack:可調節初始容量,對于可動態擴展和收縮的,可調節最大、最小容量。
Java Heap:可調節初始容量,對于可動態擴展和收縮的,可調節最大、最小容量。
方法區:可調節初始容量,對于可動態擴展和收縮的,可調節最大、最小容量。
本地方法棧:可調節初始容量,對于可動態擴展和收縮的,可調節最大、最小容量。
參考
1、《Java虛擬機規范SE7》第2章 Java虛擬機結構
2、《深入理解Java虛擬機》第2章 Java內存區域