Java虛擬機(jī)在執(zhí)行Java程序的過程中,會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域宅此。這些區(qū)域都有各自的用途喜每,以及創(chuàng)建和銷毀的時(shí)間
——摘自《深入理解Java虛擬機(jī)》第2版
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定箕慧,Java虛擬機(jī)會(huì)把所管理的內(nèi)存劃分為以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū):程序計(jì)數(shù)器歉胶、堆、方法區(qū)劝篷、虛擬機(jī)棧哨鸭、本地方法區(qū),如下圖所示:
程序計(jì)數(shù)器
占用一塊較小的內(nèi)存空間娇妓,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的 行號(hào)指示器像鸡,字節(jié)碼解釋器工作時(shí)通過改變?cè)撚?jì)數(shù)器的值來選擇下一條需要執(zhí)行的字節(jié)碼指令,分支峡蟋、循環(huán)坟桅、跳轉(zhuǎn)、異常處理蕊蝗、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器來完成仅乓。
每條線程都擁有一個(gè)獨(dú)立的的程序計(jì)數(shù)器,各線程之間的計(jì)數(shù)器互不影響蓬戚,獨(dú)立存儲(chǔ)
這樣的內(nèi)存區(qū)域夸楣,我們稱之為“線程私有”的內(nèi)存,線程私有的內(nèi)存區(qū)域子漩,不會(huì)發(fā)生并發(fā)問題豫喧。
當(dāng)線程在執(zhí)行一個(gè)Java方法時(shí),該計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址幢泼。
當(dāng)線程在執(zhí)行的是Native方法(調(diào)用本地操作系統(tǒng)方法)時(shí)紧显,該計(jì)數(shù)器的值為空(Undefined)。
該內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何內(nèi)存溢出(OutOfMemoryError)情況的區(qū)域缕棵。
Java虛擬機(jī)棧
與程序計(jì)數(shù)器一樣孵班,Java虛擬機(jī)棧也是 線程私有 的涉兽,它的生命周期與線程相同,即同生共死篙程。
可能發(fā)生的兩種異常:
StackOverflowError異常條件:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度
OutOfMemoryError異常條件:虛擬機(jī)椉衔罚可以動(dòng)態(tài)擴(kuò)展,但是擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀虱饿,用于存儲(chǔ)局部變量表拥诡、操作數(shù)棧、動(dòng)態(tài)鏈接氮发、方法出口等信息渴肉。每一個(gè)方法從調(diào)用開始到最后的結(jié)束,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧出棧的過程
虛擬機(jī)棧既然描述的是Java方法執(zhí)行的內(nèi)存模型爽冕,也就是說宾娜,虛擬機(jī)棧這塊內(nèi)存存儲(chǔ)的內(nèi)容是跟方法里的內(nèi)容相關(guān)的。
比如現(xiàn)在有下面一個(gè)方法:
public static void main(String[] args) {for (int a = 0, len = 2018; a < len; a++) {System.out.println("a = " + a);}}
這個(gè)方法中的所有內(nèi)容就是存儲(chǔ)在棧幀中扇售。棧幀又包含:局部變量表、操作數(shù)棧嚣艇、動(dòng)態(tài)鏈接承冰、方法出口等信息。它們各自的職能如下:
1食零、局部變量表
顧名思義困乒,局部變量表是一塊存放了 編譯期 可知的基本類型(byte、boolean 贰谣、char娜搂、short、int吱抚、float百宇、long、double)秘豹、對(duì)象引用(reference類型携御,注意:不等同于對(duì)象本身)和returnAddress類型(它指向了一條字節(jié)碼指令的地址)的內(nèi)存空間。
編譯期指的是既绕,Java代碼被編譯成Class文件的過程啄刹。在這個(gè)過程中,就確定了所需分配的最大局部變量表的容量凄贩。該容量在方法運(yùn)行期間不會(huì)改變大小誓军。
說明:
64位的長(zhǎng)度的long和dobule類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間(slot,也稱變量槽)疲扎,其余的數(shù)據(jù)類型只占用1個(gè)局部變量空間昵时。
如上面main方法捷雕,那么該局部變量表的大小如下:
數(shù)組類型(特殊的引用類型)的args 占用1個(gè)slot
for循環(huán)中的變量a 占用1個(gè)slot
for循環(huán)中的變量len 占用1個(gè)slot
可這樣理解slot中存放的內(nèi)容:
人由姓名和身體組成,姓名是它的變量债查,身體是它具體的數(shù)值非区,比如張三是一個(gè)80公斤的胖子,張三是變量名盹廷,80公斤是它的值征绸,一起存在slot中
局部變量表的相關(guān)文檔參考:
https://blog.csdn.net/qq_30739519/article/details/51043512
https://blog.csdn.net/keda8997110/article/details/19480769
https://blog.csdn.net/xixiaoming_A/article/details/52260137
https://blog.csdn.net/TuGeLe/article/details/78886522
虛擬機(jī)通過索引定位的方式使用局部變量表,索引值的范圍是從0開始到局部變量表最大的Slot數(shù)量俄占,對(duì)于32位數(shù)據(jù)類型的變量管怠,索引n代表第n個(gè)Slot,對(duì)于64位的缸榄,索引n代表第n和第n+1兩個(gè)Slot渤弛。
在方法執(zhí)行時(shí),虛擬機(jī)是使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程的甚带,如果是實(shí)例方法(非static)她肯,則局部變量表中的第0位索引的Slot默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中可以通過關(guān)鍵字“this”來訪問這個(gè)隱含的參數(shù)鹰贵。其余參數(shù)則按照參數(shù)表的順序來排列晴氨,占用從1開始的局部變量Slot,參數(shù)表分配完畢后碉输,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot籽前。
局部變量表中的Slot是可重用的,方法體中定義的變量敷钾,作用域并不一定會(huì)覆蓋整個(gè)方法體枝哄,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超過了某個(gè)變量的作用域,那么這個(gè)變量對(duì)應(yīng)的Slot就可以交給其他變量使用阻荒。這樣的設(shè)計(jì)不僅僅是為了節(jié)省空間挠锥,在某些情況下Slot的復(fù)用會(huì)直接影響到系統(tǒng)的而垃圾收集行為。
摘自如下文章:
https://blog.csdn.net/ns_code/article/details/17565503
2财松、操作數(shù)棧
操作數(shù)棧又常被稱為操作棧瘪贱,操作數(shù)棧的最大深度也是在編譯的時(shí)候就確定了。32位數(shù)據(jù)類型所占的棧容量為1,64為數(shù)據(jù)類型所占的棧容量為2辆毡。當(dāng)一個(gè)方法開始執(zhí)行時(shí)菜秦,它的操作棧是空的,在方法的執(zhí)行過程中舶掖,會(huì)有各種字節(jié)碼指令(比如:加操作球昨、賦值元算等)向操作棧中寫入和提取內(nèi)容,也就是入棧和出棧操作眨攘。
Java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”主慰,其中所指的“椣荩”就是操作數(shù)棧。因此我們也稱Java虛擬機(jī)是基于棧的共螺,這點(diǎn)不同于Android虛擬機(jī)该肴,Android虛擬機(jī)是基于寄存器的。
基于棧的指令集最主要的優(yōu)點(diǎn)是可移植性強(qiáng)藐不,主要的缺點(diǎn)是執(zhí)行速度相對(duì)會(huì)慢些匀哄;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的優(yōu)點(diǎn)是執(zhí)行速度快雏蛮,主要的缺點(diǎn)是可移植性差涎嚼。
摘自如下文章:
https://blog.csdn.net/ns_code/article/details/17565503
3、動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池(在方法區(qū)中挑秉,后面介紹)中該棧幀所屬方法的引用法梯,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接。Class文件的常量池中存在有大量的符號(hào)引用犀概,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)立哑。這些符號(hào)引用,一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用(如final姻灶、static域等)刁憋,稱為靜態(tài)解析,另一部分將在每一次的運(yùn)行期間轉(zhuǎn)化為直接引用木蹬,這部分稱為動(dòng)態(tài)連接。
摘自如下文章:
https://blog.csdn.net/ns_code/article/details/17565503
4若皱、方法返回地址
當(dāng)一個(gè)方法被執(zhí)行后镊叁,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個(gè)方法返回的字節(jié)碼指令或遇到了異常,并且該異常沒有在方法體內(nèi)得到處理走触。無論采用何種退出方式晦譬,在方法退出之后,都需要返回到方法被調(diào)用的位置互广,程序才能繼續(xù)執(zhí)行敛腌。方法返回時(shí)可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)惫皱。一般來說像樊,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)器的值就可以作為返回地址旅敷,棧幀中很可能保存了這個(gè)計(jì)數(shù)器值生棍,而方法異常退出時(shí),返回地址是要通過異常處理器來確定的媳谁,棧幀中一般不會(huì)保存這部分信息涂滴。
方法退出的過程實(shí)際上等同于把當(dāng)前棧幀出站友酱,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,如果有返回值柔纵,則把它壓入調(diào)用者棧幀的操作數(shù)棧中缔杉,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
摘自如下文章:
https://blog.csdn.net/ns_code/article/details/17565503
本地方法棧
與虛擬機(jī)棧所發(fā)揮的作用非常相似搁料,區(qū)別是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)或详,而本地方法棧則為虛擬機(jī)使用到的本地Native方法服務(wù)。
可能發(fā)生的兩種異常:
StackOverflowError異常條件:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度
OutOfMemoryError異常條件:虛擬機(jī)椉釉担可以動(dòng)態(tài)擴(kuò)展鸭叙,但是擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存
Java堆
可能發(fā)生的異常:
OutOfMemoryError異常條件:堆中如果沒有內(nèi)存完成實(shí)例分配,并且堆無法再擴(kuò)展
Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊拣宏,該區(qū)域被所有的線程共享沈贝,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
該區(qū)域的唯一目的就是存放對(duì)象實(shí)例勋乾,在Java虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配宋下。
注意:
隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配辑莫、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)生学歧,所有對(duì)象都分配在堆上也漸漸變得不是那么“絕對(duì)”了。
——摘自《深入理解Java虛擬機(jī)》第2版
解釋:
我們可以這樣理解堆上存儲(chǔ)的內(nèi)容各吨,即枝笨,堆上存儲(chǔ)了幾乎所有的對(duì)象實(shí)例和數(shù)組,有可能對(duì)象以及數(shù)組不存在堆上揭蜒。
Java Heap是垃圾收集器管理的主要區(qū)域横浑,因此很多時(shí)候也被稱為“GC堆”。
從內(nèi)存回收的角度來看屉更,由于現(xiàn)在的收集器基本都采用分代收集算法徙融,所以Java堆中可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)瑰谜,還可分為:Edgn空間欺冀、From Survivor空間、To Survivor空間萨脑。
從內(nèi)存分配的角度來看隐轩,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定渤早,Java堆可以處在 物理上不連續(xù) 的內(nèi)存空間中龙助,只要邏輯上是連續(xù)的即可。
方法區(qū)
可能發(fā)生的異常:
OutOfMemoryError異常條件:當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)
與Java堆一樣,方法區(qū)也是被各個(gè)線程共享的內(nèi)存區(qū)域提鸟,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息军援、常量、靜態(tài)變量称勋、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)胸哥。
方法區(qū)域又被稱為“永久代”,但這僅僅對(duì)于Sun HotSpot來講赡鲜,JRockit和IBM J9虛擬機(jī)中并不存在永久代的概念空厌。
Java虛擬機(jī)規(guī)范把方法區(qū)描述為Java堆的一個(gè)邏輯部分,而且它和Java Heap一樣不需要連續(xù)的內(nèi)存银酬,可以選擇固定大小或可擴(kuò)展嘲更,另外,虛擬機(jī)規(guī)范允許該區(qū)域可以選擇不實(shí)現(xiàn)垃圾回收揩瞪。
相對(duì)而言赋朦,垃圾收集行為在這個(gè)區(qū)域比較少出現(xiàn)。該區(qū)域的內(nèi)存回收目標(biāo)主要針是對(duì)廢棄常量的和無用類的回收李破。
運(yùn)行時(shí)常量池是方法區(qū)的一部分宠哄,Class文件中除了有類的版本、字段嗤攻、方法毛嫉、接口等描述信息外,還有一項(xiàng)信息是常量池(Class文件常量池)妇菱,用于存放編譯器生成的各種字面量和符號(hào)引用承粤,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另一個(gè)重要特征是具備動(dòng)態(tài)性闯团,Java語言并不要求常量一定只能在編譯期產(chǎn)生密任,也就是并非預(yù)置入Class文件中的常量池的內(nèi)容才能進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中偷俭,這種特性被開發(fā)人員利用比較多的是String類的intern()方法。
直接內(nèi)存
可能發(fā)生的異常:
OutOfMemoryError異常條件:動(dòng)態(tài)擴(kuò)展時(shí)無法得到有效分配缰盏,即無法動(dòng)態(tài)擴(kuò)展時(shí)
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分涌萤,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,不受Java堆大小的限制口猜,但是會(huì)受到本機(jī)總內(nèi)存的大小及處理器尋址空間的限制负溪。
總結(jié):
Java的內(nèi)存區(qū)域按照共享與否可以分為兩大類:
① 線程私有內(nèi)存區(qū)域,即济炎,伴隨線程而生川抡,伴隨線程而銷毀:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧
②線程共享內(nèi)存區(qū)域崖堤,即侍咱,多個(gè)線程進(jìn)行空間共享的區(qū)域:堆和方法區(qū)