Java內(nèi)存模型
Java內(nèi)存模型爹梁,就是Java程序運(yùn)行時(shí)的內(nèi)存模型。而Java代碼是在Java虛擬機(jī)上運(yùn)行的,由Java虛擬機(jī)解釋執(zhí)行(解釋器)或者編譯執(zhí)行(即使編譯器同眯,JIT)來(lái)完成。所以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ī)來(lái)處理內(nèi)存柿估。虛擬機(jī)有垃圾回收(GC)循未,會(huì)去自動(dòng)進(jìn)行垃圾回收。那么都是自動(dòng)的秫舌,好學(xué)什么的妖?其實(shí)虛擬機(jī)自動(dòng)回收垃圾有一套自己的規(guī)律,并不能根據(jù)程序猿編寫(xiě)的代碼隨時(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ī)的解釋器通過(guò)改變當(dāng)前線程程序計(jì)數(shù)器的值來(lái)按照流程執(zhí)行指令嫡良。當(dāng)然還有一個(gè)好處體現(xiàn)在多線程方面。一個(gè)內(nèi)核同一時(shí)間只能執(zhí)行一條指令献酗,多線程的工作是依靠線程輪流切換(占用CPU時(shí)間)來(lái)達(dá)到的寝受。對(duì)于每一個(gè)線程來(lái)說(shuō),必須有一個(gè)地方存儲(chǔ)中斷時(shí)的地址(行號(hào))罕偎,才能保證切回來(lái)時(shí)回到正確地址很澄。Java規(guī)范沒(méi)有規(guī)定該內(nèi)存區(qū)域OOM(OutOfMemoryError)異常。
注意颜及,上面說(shuō)的指令指的是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é)束的過(guò)程酌呆,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。
在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ò)展到無(wú)法申請(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)的棧元素。
在編譯代碼的時(shí)候度迂,棧幀中需要多大的局部變量表藤乙,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫(xiě)入到了方法表的Code屬性中惭墓,因此一個(gè)棧幀需要分配多少內(nèi)存坛梁,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體虛擬機(jī)的實(shí)現(xiàn)腊凶。
對(duì)于執(zhí)行引擎來(lái)講划咐,活動(dòng)線程中,只有虛擬機(jī)棧頂?shù)臈攀怯行У木迹Q為當(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)容:
- 局部變量表队魏,是一組變量值存儲(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é)碼指令地址)
- 操作數(shù)棧呢诬,操作數(shù)棧也常被稱為操作棧,它是一個(gè)后入先出棧胖缤。同局部變量表一樣尚镰,操作數(shù)棧的最大深度也是編譯的時(shí)候被寫(xiě)入到方法表的Code屬性的max_stacks數(shù)據(jù)項(xiàng)中。
- 動(dòng)態(tài)鏈接哪廓,每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬性方法的引用狗唉,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(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)連接。
- 方法返回地址剧劝,當(dāng)一個(gè)方法被執(zhí)行后橄登,有兩種方式退出。
- 執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令讥此,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的的方法稱為調(diào)用者)拢锹。是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來(lái)決定,這種退出方法方式稱為正常完成出口(Normal Method Invocation Completion)萄喳。
- 在方法執(zhí)行過(guò)程中遇到了異常卒稳,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到處理。只要該異常在本方法的異常表中沒(méi)有搜索到匹配的異常處理器他巨,就會(huì)導(dǎo)致方法退出充坑,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出染突,是不會(huì)給它的調(diào)用都產(chǎn)生任何返回值的捻爷。
- 附加信息,虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒(méi)有描述的信息到棧幀中份企,例如與高度相關(guān)的信息也榄,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)。在實(shí)際開(kāi)發(fā)中司志,一般會(huì)把動(dòng)態(tài)連接甜紫,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息俐芯。
執(zhí)行引擎棵介,Java虛擬機(jī)的執(zhí)行引擎在執(zhí)行代碼時(shí),都有解釋執(zhí)行(通過(guò)解釋器執(zhí)行)和編譯執(zhí)行(通過(guò)JIT產(chǎn)生本地代碼(特定CPU機(jī)器碼)執(zhí)行)兩種選擇吧史。
以上內(nèi)容參考自深入理解Java虛擬機(jī)筆記---運(yùn)行時(shí)棧幀結(jié)構(gòu)邮辽,只是做到了了解棧幀的結(jié)構(gòu)。
日常開(kāi)發(fā)中贸营,經(jīng)常把內(nèi)存模型分為堆和棧吨述,而其中的棧其實(shí)就是虛擬機(jī)棧,更準(zhǔn)確的說(shuō)是虛擬機(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)存回收的角度來(lái)看莲镣,Java堆可以細(xì)分為新生代和老年代(舊生代)福稳。
- 從內(nèi)存分配的角度來(lái)看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)瑞侮。
進(jìn)一步劃分的目的是為了更好的回收內(nèi)存灵寺。
在32位系統(tǒng)上最大為2G,64位系統(tǒng)上無(wú)限制区岗÷园澹可通過(guò)-Xms和-Xmx控制,-Xms為JVM啟動(dòng)時(shí)申請(qǐng)的最小Heap內(nèi)存慈缔,-Xmx為JVM可申請(qǐng)的最大Heap內(nèi)存叮称。
如果堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí)藐鹤,將會(huì)拋出OutOfMemoryError異常瓤檐。
方法區(qū)
它主要存放一杯虛擬機(jī)加載的類信息,常量娱节,靜態(tài)變量挠蛉,即使編譯器后的代碼等數(shù)據(jù)。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定肄满,當(dāng)方法區(qū)無(wú)法滿足內(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ù)庫(kù)直接分配堆外內(nèi)存问潭。它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存猿诸。
由于它是本機(jī)內(nèi)存(Ram),肯定會(huì)受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制狡忙。所以在動(dòng)態(tài)擴(kuò)展時(shí)會(huì)出現(xiàn)OOM異常梳虽。
雜記
變量的初始化
變量根據(jù)作用域分有局部變量和類變量。局部變量不像類變量那樣存在“準(zhǔn)備階段”灾茁。類變量有兩次賦初始值的過(guò)程窜觉,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值北专;另外一次在初始化階段禀挫,賦予程序員定義的值。因此即使在初始化階段程序員沒(méi)有為類變量賦值也沒(méi)有關(guān)系拓颓,類變量仍然具有一個(gè)確定的初始值语婴。但局部變量就不一樣了,如果一個(gè)局部變量定義了但沒(méi)有賦初始值是不能使用的驶睦。
字面量砰左,常量,直接量场航,符號(hào)引用類型
- 字面量:與Java語(yǔ)言層面的常量概念相近缠导,包含文本字符串、聲明為final的常量值等溉痢。還包含了基本數(shù)據(jù)類型的直接量僻造,以及引用類型的空指向。
- 符號(hào)引用:編譯語(yǔ)言層面的概念孩饼,包括以下3類:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
直接量嫡意,指在程序中直接出線的常量值。
遺留問(wèn)題
只有程序計(jì)數(shù)器內(nèi)存區(qū)域不會(huì)拋出OOM異常捣辆,其它能夠拋出OOM異常的內(nèi)存區(qū)域蔬螟,都有動(dòng)態(tài)擴(kuò)展這一特性。