Java內(nèi)存模型是每個(gè)java程序員必須掌握理解的袍祖,這是Java的核心基礎(chǔ)疏叨,對(duì)我們編寫代碼特別是并發(fā)編程時(shí)有很大幫助斯议。由于Java程序是交由JVM執(zhí)行的莲兢,所以我們?cè)谡凧ava內(nèi)存區(qū)域劃分的時(shí)候事實(shí)上是指JVM內(nèi)存區(qū)域劃分。
首先偏序,我們回顧一下Java程序執(zhí)行流程:
如上圖所示页畦,首先Java源代碼文件(.java后綴)會(huì)被Java編譯器編譯為字節(jié)碼文件(.class后綴),然后由JVM中的類加載器加載各個(gè)類的字節(jié)碼文件研儒,加載完畢之后豫缨,交由JVM執(zhí)行引擎執(zhí)行。在整個(gè)程序執(zhí)行過程中端朵,JVM會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息好芭,這段空間一般被稱作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存冲呢。因此舍败,在Java中我們常常說到的內(nèi)存管理就是針對(duì)這段空間進(jìn)行管理(如何分配和回收內(nèi)存空間)。
那么本篇文章主要是要分析Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū))的結(jié)構(gòu)碗硬。
- 運(yùn)行時(shí)數(shù)據(jù)區(qū)分為幾個(gè)部分瓤湘?
根據(jù) JVM 規(guī)范,JVM 內(nèi)存共分為虛擬機(jī)棧恩尾、堆、方法區(qū)挽懦、程序計(jì)數(shù)器翰意、本地方法棧五個(gè)部分。
名稱
特征
作用
配置參數(shù)
異常
程序計(jì)數(shù)器
占用內(nèi)存小信柿,線程私有冀偶,
生命周期與線程相同
大致為字節(jié)碼行號(hào)指示器
無
無
虛擬機(jī)棧
線程私有,生命周期與線程相同渔嚷,使用連續(xù)的內(nèi)存空間
Java 方法執(zhí)行的內(nèi)存模型进鸠,存儲(chǔ)局部變量表、操作棧形病、動(dòng)態(tài)鏈接客年、方法出口等信息
-Xss
StackOverflowError
OutOfMemoryError
java堆
線程共享霞幅,生命周期與虛擬機(jī)相同,可以不使用連續(xù)的內(nèi)存地址
保存對(duì)象實(shí)例量瓜,所有對(duì)象實(shí)例(包括數(shù)組)都要在堆上分配
-Xms
-Xsx
-Xmn
OutOfMemoryError
方法區(qū)
線程共享司恳,生命周期與虛擬機(jī)相同,可以不使用連續(xù)的內(nèi)存地址
存儲(chǔ)已被虛擬機(jī)加載的類信息绍傲、常量扔傅、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)
-XX:PermSize:
16M
-XX:MaxPermSize
64M
OutOfMemoryError
運(yùn)行時(shí)常量池
方法區(qū)的一部分烫饼,具有動(dòng)態(tài)性
存放字面量及符號(hào)引用
1.1 方法區(qū)
方法區(qū)是java虛擬機(jī)規(guī)范去中定義的一種概念上的區(qū)域猎塞,具有什么功能,但并沒有規(guī)定這個(gè)區(qū)域到底應(yīng)該位于何處杠纵,因此對(duì)于實(shí)現(xiàn)者來說荠耽,如何來實(shí)際方法區(qū)是有著很大自由度的。
永生代是hotspot中的一個(gè)概念淡诗,其他jvm實(shí)現(xiàn)未必有骇塘,例如jrockit就沒這東西。java8之前韩容,hotspot使用在內(nèi)存中劃分出一塊區(qū)域來存儲(chǔ)類的元信息款违、類變量以及內(nèi)部字符串(interned string)等內(nèi)容,稱之為永生代群凶,把它作為方法區(qū)來使用插爹。
[JEP122][2]提議取消永生代,方法區(qū)作為概念上的區(qū)域仍然存在请梢。原先永生代中類的元信息會(huì)被放入本地內(nèi)存(元數(shù)據(jù)區(qū)赠尾,metaspace),將類的靜態(tài)變量和內(nèi)部字符串放入到j(luò)ava堆中毅弧。
為了弄清楚方法區(qū)那么需要解釋兩個(gè)名詞:永久代和元空間
PermGen(永久代)
絕大部分Java程序員應(yīng)該都見過“java.lang.OutOfMemoryError: PremGen space”異常气嫁。這里的“PermGen space”其實(shí)指的就是方法區(qū)。不過方法區(qū)和“PermGen space”又有著本質(zhì)的區(qū)別够坐。前者是JVM的規(guī)范寸宵,而后者則是JVM規(guī)范的一種實(shí)現(xiàn),并且只有HotSpot才有“PermGen space”元咙,而對(duì)于其他類型的虛擬機(jī)梯影,如JRockit(Oracle)、J9(IBM)并沒有“PermGen space”庶香。由于方法區(qū)主要存儲(chǔ)類的相關(guān)信息甲棍,所以對(duì)于動(dòng)態(tài)生成類的情況比較容易出現(xiàn)永久代的內(nèi)存溢出。并且JDK 1.8中參數(shù)PermSize和MaxPermSize已經(jīng)失效赶掖。
元空間
其實(shí)感猛,移除永久代的工作從JDK 1.7就開始了七扰。JDK 1.7中,存儲(chǔ)在永久代的部分?jǐn)?shù)據(jù)就已經(jīng)轉(zhuǎn)移到Java Heap或者Native Heap唱遭。但永久代仍存在于JDK 1.7中戳寸,并沒有完全移除,譬如符號(hào)引用(Symbols)轉(zhuǎn)移到了native heap拷泽;字面量(interned strings)轉(zhuǎn)移到了Java heap疫鹊;類的靜態(tài)變量(class statics)轉(zhuǎn)移到了Java heap。
JDK1.8對(duì)JVM架構(gòu)的改造將類元數(shù)據(jù)放到本地內(nèi)存中司致,另外拆吆,將常量池和靜態(tài)變量放到Java堆里。HotSpot VM將會(huì)為類的元數(shù)據(jù)明確分配和釋放本地內(nèi)存脂矫。在這種架構(gòu)下枣耀,類元信息就突破了原來-XX:MaxPermSize的限制,現(xiàn)在可以使用更多的本地內(nèi)存庭再。這樣就從一定程度上解決了原來在運(yùn)行時(shí)生成大量類造成經(jīng)常Full GC問題捞奕,如運(yùn)行時(shí)使用反射、代理等拄轻。所以升級(jí)以后Java堆空間可能會(huì)增加颅围。
元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)恨搓。不過元空間與永久代之間的最大區(qū)別在于:元空間并不在虛擬機(jī)中院促,而是使用本地內(nèi)存。因此斧抱,默認(rèn)情況下常拓,元空間的大小僅受本地內(nèi)存限制,但可以通過以下參數(shù)指定元空間的大谢云帧:
-XX:MetaspaceSize弄抬,初始空間大小,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載宪郊,同時(shí)GC會(huì)對(duì)改值進(jìn)行調(diào)整:如果釋放了大量的空間眉睹,就適當(dāng)降低該值;如果釋放了很少的空間废膘,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值慕蔚。
-XX:MaxMetaspaceSize丐黄,最大空間,默認(rèn)是沒有限制的孔飒。
除了上面的兩個(gè)指定大小的選項(xiàng)外灌闺,還有兩個(gè)與GC相關(guān)的屬性:
-XX:MinMetaspaceFreeRatio艰争,在GC之后,最小的Metaspace剩余空間容量的百分比桂对,減少為分配空間所導(dǎo)致的垃圾收集甩卓。
-XX:MaxMetaspaceFreeRatio,在GC之后蕉斜,最大的Metaspace剩余空間容量的百分比逾柿,減少為釋放空間所導(dǎo)致的垃圾收集。
所以對(duì)于方法區(qū)宅此,Java8之后的變化:
移除了永久代(PermGen)机错,替換為元空間(Metaspace);
永久代中的 class metadata 轉(zhuǎn)移到了 native memory(本地內(nèi)存父腕,而不是虛擬機(jī))弱匪;
永久代中的 interned Strings 和 class static variables 轉(zhuǎn)移到了 Java heap;
永久代參數(shù) (PermSize MaxPermSize) -> 元空間參數(shù)(MetaspaceSize MaxMetaspaceSize)
1.2 虛擬機(jī)棧(線程棧)與 堆(Heap)
為更好的理解Java線程棧和堆璧亮,我們簡(jiǎn)單的認(rèn)為Java內(nèi)存模型把Java虛擬機(jī)內(nèi)部劃分為線程棧和堆萧诫。這張圖演示了Java內(nèi)存模型的邏輯視圖。
每一個(gè)運(yùn)行在Java虛擬機(jī)里的線程都擁有自己的線程棧枝嘶。這個(gè)線程棧包含了這個(gè)線程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息帘饶。一個(gè)線程僅能訪問自己的線程棧。一個(gè)線程創(chuàng)建的本地變量對(duì)其它線程不可見躬络,僅自己可見尖奔。即使兩個(gè)線程執(zhí)行同樣的代碼,這兩個(gè)線程任然在在自己的線程棧中的代碼來創(chuàng)建本地變量穷当。因此提茁,每個(gè)線程擁有每個(gè)本地變量的獨(dú)有版本。
所有原始類型的本地變量都存放在線程棧上馁菜,因此對(duì)其它線程不可見茴扁。一個(gè)線程可能向另一個(gè)線程傳遞一個(gè)原始類型變量的拷貝,但是它不能共享這個(gè)原始類型變量自身汪疮。
堆上包含在Java程序中創(chuàng)建的所有對(duì)象峭火,無論是哪一個(gè)對(duì)象創(chuàng)建的。這包括原始類型的對(duì)象版本智嚷。如果一個(gè)對(duì)象被創(chuàng)建然后賦值給一個(gè)局部變量卖丸,或者用來作為另一個(gè)對(duì)象的成員變量,這個(gè)對(duì)象任然是存放在堆上盏道。
下面這張圖演示了調(diào)用棧和本地變量存放在線程棧上稍浆,對(duì)象存放在堆上。
一個(gè)本地變量可能是原始類型,在這種情況下衅枫,它總是“呆在”線程棧上嫁艇。
一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用。在這種情況下弦撩,引用(這個(gè)本地變量)存放在線程棧上步咪,但是對(duì)象本身存放在堆上。
一個(gè)對(duì)象可能包含方法益楼,這些方法可能包含本地變量猾漫。這些本地變量任然存放在線程棧上,即使這些方法所屬的對(duì)象存放在堆上偏形。
一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上静袖。不管這個(gè)成員變量是原始類型還是引用類型。
靜態(tài)成員變量跟隨著類定義一起也存放在堆上俊扭。
存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線程訪問队橙。當(dāng)一個(gè)線程可以訪問一個(gè)對(duì)象時(shí),它也可以訪問這個(gè)對(duì)象的成員變量萨惑。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法捐康,它們將會(huì)都訪問這個(gè)對(duì)象的成員變量,但是每一個(gè)線程都擁有這個(gè)本地變量的私有拷貝庸蔼。
下圖演示了上面提到的點(diǎn):
兩個(gè)線程擁有一些列的本地變量解总。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)。這兩個(gè)線程分別擁有同一個(gè)對(duì)象的不同引用姐仅。這些引用都是本地變量花枫,因此存放在各自線程的線程棧上。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象掏膏。
注意劳翰,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過在Object3中這些成員變量引用馒疹,這兩個(gè)線程就可以訪問Object2和Object4佳簸。
1.3 程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器颖变。分支生均、循環(huán)、跳轉(zhuǎn)腥刹、異常處理马胧、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
由于Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的衔峰,在任何一個(gè)確定的時(shí)刻漓雅,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程中的指令录别。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置邻吞,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間的計(jì)數(shù)器互不影響葫男,獨(dú)立存儲(chǔ)抱冷,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè)Java 方法梢褐,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址旺遮;如果正在執(zhí)行的是Natvie 方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)盈咳。
此內(nèi)存區(qū)域是唯一一個(gè)在Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域耿眉。
1.4 本地方法棧
本地方法棧(Native MethodStacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù)鱼响,而本地方法棧則是為虛擬機(jī)使用到的Native 方法服務(wù)鸣剪。虛擬機(jī)規(guī)范中對(duì)本地方法棧中的方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定丈积,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它筐骇。甚至有的虛擬機(jī)(譬如Sun HotSpot 虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。
與虛擬機(jī)棧一樣江滨,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常铛纬。