前言
下面關(guān)于 Java 的內(nèi)存區(qū)域介紹大部分參考深入理解Java虛擬機(jī),也參考了網(wǎng)上很多資料沦偎,以下圖片均摘自網(wǎng)絡(luò)
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Java虛擬機(jī)在執(zhí)行 Java 程序的過(guò)程中會(huì)把它管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。根據(jù)《Java 虛擬機(jī)規(guī)范》將 Java虛擬機(jī)所管理的內(nèi)存分為以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
- 程序計(jì)數(shù)器
- Java虛擬機(jī)棧
- 本地方法棧
- Java堆
- 方法區(qū)
程序計(jì)數(shù)器
程序計(jì)數(shù)器 提茁,也稱作 PC寄存器或者指令地址寄存器情萤。在匯編語(yǔ)言中,它保存的是程序當(dāng)前執(zhí)行的指令的地址(或者說(shuō)是保存一條)川尖,當(dāng)CPU需要執(zhí)行指令時(shí),需要從程序計(jì)數(shù)器中得到當(dāng)前需要執(zhí)行的指令所在存儲(chǔ)單元的地址茫孔,然后根據(jù)得到的地址獲取指令叮喳,在得到指令之后,程序計(jì)數(shù)器便自動(dòng)加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址缰贝,如此循環(huán)馍悟,直至執(zhí)行完所有的指令。
在JVM中剩晴,程序計(jì)數(shù)器是一塊較小的內(nèi)存空間锣咒,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器侵状,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取嚇一跳需要執(zhí)行的字節(jié)碼指令。
由于Java 虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的毅整,在任何一個(gè)確定的時(shí)間趣兄,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此悼嫉,為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置艇潭,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響戏蔑,獨(dú)立存儲(chǔ)蹋凝,這是 "線程私有的"。
如果線程正在執(zhí)行的是一個(gè) Java方法总棵,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native方法仙粱,計(jì)數(shù)器值為空,此內(nèi)存區(qū)域是唯一一個(gè)在 Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何 OutOfMemoryError
情況的區(qū)域彻舰。
Java虛擬機(jī)棧
Java虛擬機(jī)棧也是線程私有的伐割,它的生命周期與線程相同,它描述的是 Java 方法執(zhí)行的內(nèi)存模型: 每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀( Stack Frame)用于存儲(chǔ)局部變量表刃唤,操作數(shù)棧隔心,動(dòng)態(tài)鏈接,方法出口等信息尚胞。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程硬霍,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。至于關(guān)于棧幀的具體介紹后續(xù)文章再分析笼裳。
因?yàn)槌藯某鰲:腿霔V馕簦琂ava虛擬機(jī)棧不會(huì)再受其他因素的影響,所以 棧幀可以在系統(tǒng)的堆中分配(注意躬柬,是系統(tǒng)的Heap而不是Java 堆)
JVM保留了兩個(gè)內(nèi)存區(qū):Java 堆和本機(jī)(或系統(tǒng)堆)拜轨。這個(gè)堆具有不同的用途,并使用不同的機(jī)制進(jìn)行維護(hù)允青,Java堆就是我下面要將的包含對(duì)象實(shí)例的"堆",而系統(tǒng)的堆使用操作系統(tǒng)的底層
malloc
和 free機(jī)制進(jìn)行分配橄碾,且用于底層實(shí)施特定的Java對(duì)象。
Java虛擬機(jī)棧所使用的內(nèi)存不需要保證是連續(xù)的颠锉。
Java虛擬機(jī)椃ㄉ可能發(fā)生如下異常情況:
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出
StackOverflowError
異常 - 如果虛擬機(jī)椙砺樱可以動(dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可以動(dòng)態(tài)擴(kuò)展拒垃,只不過(guò)Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存瓷蛙,就會(huì)拋出
OutOfMemoryError
異常悼瓮。
注意: Java虛擬機(jī)棧就是棧怜森,也可以成為"堆棧",只是堆棧這種說(shuō)法容易讓人混淆。關(guān)于Java虛擬機(jī)的堆谤牡,棧,堆棧如何去理解這類問(wèn)題姥宝,JVM專家R大也在知乎上對(duì)其進(jìn)行了詳細(xì)的解答翅萤,傳送門: Java虛擬機(jī)的堆、棧腊满、堆棧如何去理解套么? - RednaxelaFX的回答 - 知乎
本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過(guò)是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java方法(也就是字節(jié)碼)服務(wù)碳蛋,而本地方法棧則為虛擬機(jī)使用到的是 Native
方法服務(wù)胚泌。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言,使用方法與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有強(qiáng)制規(guī)定肃弟,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它玷室。
看到這個(gè)本地方法棧,總是對(duì)本地方法有些疑惑笤受,下面簡(jiǎn)單說(shuō)下Native Method:
什么是 Native Method
一個(gè)Natvie Method就是一個(gè)Java調(diào)用非Java代碼的接口穷缤,它由非Java語(yǔ)言實(shí)現(xiàn),比如C語(yǔ)言
在定義一個(gè) Native Method時(shí)箩兽,并不提供實(shí)現(xiàn)體(有些像定義一個(gè) Java Interface),因?yàn)槠鋵?shí)現(xiàn)體是由非Java語(yǔ)言在外面實(shí)現(xiàn)的,比如:
public class IHaveNatives
{
native public void Native1( int x ) ;
native static public long Native2() ;
native synchronized private float Native3( Object o ) ;
native void Native4( int[] ary ) throws Exception ;
}
native方法可以返回任何 Java類型津肛,也能夠?qū)崿F(xiàn)異常控制汗贫。
為什么使用Native Method
Java對(duì)一些層次的任務(wù)用 Java實(shí)現(xiàn)不容易身坐,對(duì)某些程序效率不高:
- Java與Java外的環(huán)境交互:
Java與一些底層系統(tǒng)如操作系統(tǒng)或某些硬件交換信息,native方法提供一個(gè)非常簡(jiǎn)潔的接口落包,無(wú)需了解Java應(yīng)用之外的細(xì)節(jié)部蛇。
- 與操作系統(tǒng)交互
通過(guò)使用本地方法讓 Java實(shí)現(xiàn) JRE與底層系統(tǒng)的交互
- Sun'Java
Sun的解釋器是用 C實(shí)現(xiàn)的,JRE 大部分用 Java實(shí)現(xiàn)咐蝇,其通過(guò)一些本地方法與外界交互
所以對(duì)于本地方法棧來(lái)說(shuō)搪花,它本質(zhì)是為本地方法服務(wù)的,如果某個(gè)虛擬機(jī)實(shí)現(xiàn)的本地方法接口是使用 C連接模型的話嘹害,那么它的本地方法棧就是 C棧撮竿。下圖展示了一個(gè)Java棧和本地方法棧之間的跳轉(zhuǎn),(圖片摘自網(wǎng)絡(luò)):
Java堆
對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java 堆(Java Heap)是Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊笔呀。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域幢踏,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例许师,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存
Java堆是垃圾收集器管理的主要區(qū)域房蝉,因此很多時(shí)候也被稱作為 "GC堆"僚匆。從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本采用分代收集算法搭幻,(關(guān)于垃圾算法的介紹后續(xù)文章分析)咧擂,所以 Java 堆中還可以細(xì)分為: 新生代和老年代,再細(xì)致一點(diǎn)有 Eden空間檀蹋,F(xiàn)rom Survivor空間松申,To Survivor 空間等
根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定, Java 堆可以處于物理不連續(xù)的內(nèi)存空間中俯逾,只要邏輯是連續(xù)的即可贸桶,就像我們的磁盤空間一樣。在實(shí)現(xiàn)時(shí)桌肴,即可以實(shí)現(xiàn)成固定大小的皇筛,也可以是可擴(kuò)展的,不過(guò)當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的 (通過(guò) -Xmx 和 -Xms 控制)坠七。
如果堆上沒(méi)有內(nèi)存完成實(shí)例分配水醋,并且 堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出 OutOfMemoryError
異常彪置。
JVM中堆和棧的區(qū)別
這里簡(jiǎn)單說(shuō)說(shuō)JVM中堆和棧的區(qū)別:
- 功能不同
- 棧內(nèi)存用來(lái)存儲(chǔ)局部變量离例,操作數(shù)棧等信息
- 堆內(nèi)存用來(lái)存儲(chǔ)Java中的對(duì)象
- 共享性不同
- 棧內(nèi)存是線程私有的
- 堆內(nèi)存是所有線程共有的
- 空間大小
- 棧的空間大小遠(yuǎn)遠(yuǎn)小于堆的
- 異常錯(cuò)誤不同
- 棧有兩種異常情況,如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度悉稠,將拋出
StackOverflowError
異常宫蛆,如果虛擬機(jī)棧動(dòng)態(tài)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError
異常的猛。 - 堆一般在堆中沒(méi)有內(nèi)存完成實(shí)例分配耀盗,并且堆也無(wú)法進(jìn)行擴(kuò)展時(shí),拋出
OutOfMemoryError
異常卦尊。
- 棧有兩種異常情況,如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度悉稠,將拋出
方法區(qū)
方法區(qū)(Method Area) 與 Java堆一樣叛拷,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息岂却,常量忿薇,靜態(tài)變量,即時(shí)編譯器編譯后的代碼等數(shù)據(jù)躏哩。雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為 堆的一個(gè)邏輯部分署浩,但是它有一個(gè)別名叫做 "非堆".
對(duì)于HotSpot虛擬機(jī)來(lái)講,方法區(qū)域又被稱為 "永久代"扫尺,本質(zhì)上兩者并不等價(jià)筋栋,僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把 GC 分代收集擴(kuò)展至方法區(qū),或者說(shuō)使用 永久代來(lái)實(shí)現(xiàn)方法區(qū)而已正驻,這樣的HotSpot的垃圾收集器可以像 管理 Java堆一樣管理這部分內(nèi)存弊攘,能夠省去專門為方法區(qū)編寫(xiě)內(nèi)存管理代碼的工作抢腐。對(duì)于其他虛擬機(jī)(如 BEA JRockit,IBM J9等)來(lái)說(shuō)是不存在永久代的概念的。
在JDK 1.7及以前的HotSpot JVM中襟交,方法區(qū)位于永久代(Permanent Generation,PermGen) 中迈倍。如下圖,是JDK 1.7及以前的 Java堆內(nèi)存的結(jié)構(gòu)圖捣域,里面包含了 Permanent Generation:
由于 永久代內(nèi)可能發(fā)生內(nèi)存泄漏或溢出的問(wèn)題(永久代有 -XX:MaxPermSize的上限)而導(dǎo)致的 java.lang.OutOfMemoryError: PermGen space
,JEP小組從JDK 1.7 開(kāi)始就籌劃移除永久代 (JEP 122: Remove the Permanent Generation,并且在 JDK 1.7 中把字符串常量啼染,符號(hào)引用等移除了永久代。到了Java 8 ,永久代被徹底地移除了 JVM,取而代之的是元空間 (Metaspace):
JDK 8開(kāi)始將類的元數(shù)據(jù)放到本地堆內(nèi)存(native heap)中竟宋,這一塊區(qū)域就叫 Metaspace,關(guān)于
Metaspace
的介紹請(qǐng)參考 Metaspace in Java 8
運(yùn)行時(shí)常量池
前面講 方法區(qū)的時(shí)候就提到,運(yùn)行時(shí)常量池是方法區(qū)的一部分形纺,它是 class文件中每一個(gè)類或接口的常量池表的運(yùn)行時(shí)表示形式丘侠。它包括了若干種不同的常量,常量池表存放 編譯器生成的各種字面量和符號(hào)引用逐样,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放蜗字。
運(yùn)行時(shí)常量池具有動(dòng)態(tài)性,運(yùn)行期間也可以將新的量放到運(yùn)行時(shí)常量池中脂新,典型的應(yīng)用是 String 類的 intern方法:
public native String intern()
String
類的 intern
方法會(huì)從字符串常量池中查詢當(dāng)前字符串是否存在挪捕,如果存在,就會(huì)直接返回當(dāng)前字符串争便,若不存在就會(huì)將當(dāng)前字符串放入常量池中级零,再返回。關(guān)于String.intern()
更為詳盡的分析滞乙,請(qǐng)參閱文章: 深入解析String#intern
而從JDK 1.7開(kāi)始奏纪,字符串常量和符號(hào)引用等被移除永久代:
- 符號(hào)引用遷移至系統(tǒng)堆內(nèi)存 (Native Heap)
- 字符串字面量遷移至 Java堆(Java Heap)
小結(jié)
以上的分析參考了深入理解Java虛擬機(jī)這本書(shū),同時(shí)也參考了很多優(yōu)秀的文章斩启。在此過(guò)程中序调,我們要注意JDK版本變化帶來(lái)的問(wèn)題,比如在 JDK 8版本中兔簇,永久代被徹底移除了发绢。
當(dāng)上面提到一個(gè)JVM的巨牛級(jí)別的人物——R大,R大是國(guó)內(nèi)JVM巨牛級(jí)人物垄琐,他的回答都是非常權(quán)威的边酒,所以學(xué)習(xí) JVM的知識(shí)可以多參考 R大的分析。目前本人對(duì) JVM也是一枚渣渣級(jí)選手狸窘,現(xiàn)在輸出對(duì)JVM的一些學(xué)習(xí)筆記甚纲。如有錯(cuò)誤之處,歡迎指出朦前。