注1:以下所提及線程,無特定說明的均默認指代“Java虛擬機線程”。
注2:注意避免混淆Stack、Heap和Java(VM) Stack迈套、Java Heap的概念。Java虛擬機是在操作系統(tǒng)之上的更高層抽象碱鳞,可以看作是一臺虛擬的計算機桑李。Java虛擬機的內(nèi)存劃分與操作系統(tǒng)的內(nèi)存分區(qū)無法一一對應。Java虛擬機的實現(xiàn)本身是由其他語言編寫的應用程序窿给,在Java語言程序的角度上看分配在Java Stack中的數(shù)據(jù)贵白,而在實現(xiàn)虛擬機的程序角度上看則可以是分配在Heap之中。
Java虛擬機的內(nèi)存結構崩泡,區(qū)別于側重于多線程的Java內(nèi)存模型(Java?Memory?Model)禁荒。
我們當然要精確地定義每個區(qū)域的概念和用途,但在此之前(或之后)角撞,是不是應該思考一下:JVM的內(nèi)存結構為什么要這樣劃分呛伴?
私以為主要是依據(jù)不同數(shù)據(jù)的更新頻率、訪問速度要求谒所、垃圾收集管理來劃分的热康。線程的工作內(nèi)存區(qū),要求速度快劣领,也受限于高速讀寫設備資源的稀缺性姐军,JVM據(jù)此劃分了PC寄存器、Stack(棧)尖淘,用來存儲當前方法操作計數(shù)和局部變量(基本類型及引用)奕锌。對于更新頻率較低的類結構信息、常量村生、靜態(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ū)敢伸,分別對應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用于存儲局部變量與一些過程結果李茫。它在方法調(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ū)域。它存儲了每一個類的結構信息考阱,例如運行時常量池翠忠、字段和方法數(shù)據(jù)、構造函數(shù)和普通方法的字節(jié)碼內(nèi)容乞榨。方法區(qū)在虛擬機啟動的時候被創(chuàng)建秽之。
運行時常量池:該池是每一個類或接口的常量池的運行時表示形式,它包括了若干種不同的常量:從編譯期可知的數(shù)值字面值到必須運行期解析才能獲得的方法或字段引用吃既。運行時常量池扮演了類似傳統(tǒng)語言中符號表(Symbol Table)的角色考榨,不過它存儲數(shù)據(jù)范圍比通常意義上的符號表要更為廣泛。每一個運行時常量池都分配在Java虛擬機的方法區(qū)之中鹦倚,在類和接口被加載到虛擬機后河质,對應的的運行時常量池就被創(chuàng)建出來。
棧幀:Frame是用來存儲數(shù)據(jù)和部分過程結果的數(shù)據(jù)結構震叙,同時也被用來處理動態(tài)鏈接掀鹅、方法返回值和異常分派。Frame隨方法調(diào)用而創(chuàng)建媒楼,隨方法結束而銷毀——無論是正常完成或是異常完成(拋出了在方法內(nèi)無未被捕獲的異常)都算作方法結束乐尊。Frame在存儲空間分配在JVM Stack之中,每一個Frame都有自己的局部變量表划址、操作數(shù)棧和指向當前方法所屬的類的運行時常量池的引用扔嵌。
局部變量表和操作數(shù)棧的容量是在編譯期確定限府,并通過方法的Code屬性保存和提供給Frame使用。因此痢缎,F(xiàn)rame容量的大小僅僅取決于JVM的實現(xiàn)和方法調(diào)用時可被分配的內(nèi)存胁勺。
在一條線程中,只有當前正在執(zhí)行的那個方法的Frame是活動的牺弄,稱為Current Frame(當前棧幀,簡稱CF)姻几,其對應的方法稱為Current Method(簡稱CM),定義該方法的類就稱作Current Class势告。對局部變量表和操作數(shù)棧的各種操作蛇捌,通常都指的是對當前棧幀的對局部變量表和操作數(shù)棧進行的操作。
如果CM調(diào)用了其他方法咱台,或者CM執(zhí)行結束络拌,那這個方法的Frame就不再是CF了。當一個新的方法被調(diào)用回溺,一個新的Frame也會隨之而創(chuàng)建春贸,并且隨著程序控制權移交到新的方法而成為新的CF。當方法返回之際遗遵,CF會傳回方法的執(zhí)行結果給前一個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關鍵字)掀虎。后續(xù)的其他參數(shù)將會傳遞至從1開始的連續(xù)的局部變量表位置上凌盯。
操作數(shù)棧:每個Frame內(nèi)部都包含一個稱為“操作數(shù)棧”的后進先出棧(LIFO)烹玉。它的長度由編譯期決定驰怎,并且存儲于類和接口的二進制表示中,即通過方法的Code屬性保存及提供給Frame使用二打。在上下文明確县忌,不會產(chǎn)生誤解的前提下,我們經(jīng)常把“當前棧幀的操作數(shù)椉绦В”直接簡稱為“操作數(shù)椫⑿樱”。
操作數(shù)棧所屬的Frame在創(chuàng)建初時瑞信,操作數(shù)棧是空的厉颤。JVM提供一些字節(jié)碼指令來從局部變量表或對象實例的字段中復制常量或變量值到操作數(shù)棧中,也提供了一些指令用于從操作數(shù)棧中取走數(shù)據(jù)凡简、操作數(shù)據(jù)和把操作結果重新入棧逼友。在方法調(diào)用時,操作數(shù)棧也用來準備調(diào)用方法的參數(shù)以及接收方法返回結果秤涩。(過程細節(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)振定、跳轉必怜、異常處理、線程恢復等基礎功能都需要依賴這個計數(shù)器來完成后频。
由于JVM的多線程是通過輪流切換分配CPU執(zhí)行時間的方式來實現(xiàn)的梳庆,在某個特定時刻,一個CPU/內(nèi)核只會執(zhí)行一條線程的指令卑惜。為了線程切換后能恢復到正確的執(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í)行完成的過程厂抽,就對應著一個Frame在JVM Stack中入棧到出棧的過程。
經(jīng)常有人把Java內(nèi)存分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)打厘,這種分法比較粗糙修肠,Java內(nèi)存區(qū)域的劃分遠比這復雜。其中所指“椈Фⅲ”就是現(xiàn)在講的JVM Stack嵌施,或者說是JVM Stack中局部變量表部分(直接越過了Stack和Frame)。
局部變量表莽鸭,存放了編譯期可知的8大基本數(shù)據(jù)類型吗伤、對象引用(reference類型,并不是對象本身硫眨,可能只是一個指向對象起始地址的引用指針足淆,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。局部變量表所需的空間在編譯期間完成分配礁阁,當進入一個方法時巧号,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小姥闭。
當線程請求分配的棧容量超過JVM允許的最大容量時丹鸿,將拋出StackOverflowError;
如果JVM可動態(tài)擴展棚品,并且擴展的動作已經(jīng)嘗試過靠欢,但是目前無法申請到足夠的內(nèi)存去完成擴展,或在建立新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應的JVM Stack铜跑,將拋出OutOfMemoryError门怪。
StackOverflowError 表示 請求 > Stack.Max;
OutOfMemoryError 表示 請求 > 可分配內(nèi)存锅纺;
3.本地方法棧
本地方法棧與JVM Stack所發(fā)揮的作用是非常相似的掷空,區(qū)別只是JVM Stack為虛擬機執(zhí)行Java方法服務,而本地方法棧是為虛擬機使用到的Native方法服務。
在虛擬機規(guī)范中對本地方法棧中方法使用的語言拣帽、使用方式與數(shù)據(jù)結構沒有強制規(guī)定疼电,由實現(xiàn)自由選擇。甚至有的虛擬機(如HotSpot)直接將本地方法棧和JVM Stack合二為一减拭。與JVM Stack一樣,也會拋出StackOverflowError和OutOfMemoryError異常区丑。
4.Java Heap
對大多數(shù)應用來說拧粪,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)容無關,無論哪個區(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(非堆)狸臣,目的應該是與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寄存器:容量至少應當能保存一個returnAddress類型的數(shù)據(jù)或一個與平臺相關的本地指針的值呼畸。(不可調(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虛擬機結構
2棵红、《深入理解Java虛擬機》第2章 Java內(nèi)存區(qū)域