簡(jiǎn)介
Java虛擬機(jī)(即JVM)在Java程序運(yùn)行的過程中蚤假,會(huì)將它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域惠呼,這些區(qū)域有的隨著JVM的啟動(dòng)而創(chuàng)建崇众,有的隨著用戶線程的啟動(dòng)和結(jié)束而建立和銷毀紧憾。
除此之外反惕,JVM的內(nèi)存管理機(jī)制使得不需要再為每一個(gè)新的操作去刪除/免費(fèi)代碼,由機(jī)器代替程序員這樣就不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題了,但是一旦出現(xiàn)了這種問題如果不了解JVM是怎樣使用內(nèi)存的炕吸,那么排查錯(cuò)誤將會(huì)非常困難伐憾。一個(gè)基本的JVM運(yùn)行時(shí)內(nèi)存模型如下所示:
程序運(yùn)行時(shí)可能只有一個(gè)線程,也可能有多個(gè)線程共同執(zhí)行赫模,而方法區(qū)和堆是程序的所有線程所共享的內(nèi)存區(qū)域树肃,而程序寄存器、虛擬機(jī)棧和本地方法棧則是每個(gè)線程獨(dú)占的內(nèi)存區(qū)域瀑罗。
一胸嘴、程序計(jì)數(shù)器(Program Counter Register)
1.什么是程序計(jì)數(shù)器
程序計(jì)數(shù)器是一個(gè)記錄著當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
JAVA代碼編譯后的字節(jié)碼在未經(jīng)過JIT(實(shí)時(shí)編譯器)編譯前斩祭,其執(zhí)行方式是通過“字節(jié)碼解釋器”進(jìn)行解釋執(zhí)行劣像。簡(jiǎn)單的工作原理為解釋器讀取裝載入內(nèi)存的字節(jié)碼,按照順序讀取字節(jié)碼指令停忿。讀取一個(gè)指令后驾讲,將該指令“翻譯”成固定的操作,并根據(jù)這些操作進(jìn)行分支席赂、循環(huán)吮铭、跳轉(zhuǎn)等流程。
從上面的描述中,可能會(huì)產(chǎn)生程序計(jì)數(shù)器是否是多余的疑問。因?yàn)檠刂噶畹捻樞驁?zhí)行下去顷级,即使是分支跳轉(zhuǎn)這樣的流程勺爱,跳轉(zhuǎn)到指定的指令處按順序繼續(xù)執(zhí)行是完全能夠保證程序的執(zhí)行順序的。假設(shè)程序永遠(yuǎn)只有一個(gè)線程,這個(gè)疑問沒有任何問題,也就是說并不需要程序計(jì)數(shù)器。但實(shí)際上程序是通過多個(gè)線程協(xié)同合作執(zhí)行的柏肪。
首先我們要搞清楚JVM的多線程實(shí)現(xiàn)方式。JVM的多線程是通過CPU時(shí)間片輪轉(zhuǎn)(即線程輪流切換并分配處理器執(zhí)行時(shí)間)算法來實(shí)現(xiàn)的芥牌。也就是說烦味,某個(gè)線程在執(zhí)行過程中可能會(huì)因?yàn)闀r(shí)間片耗盡而被掛起,而另一個(gè)線程獲取到時(shí)間片開始執(zhí)行壁拉。當(dāng)被掛起的線程重新獲取到時(shí)間片的時(shí)候谬俄,它要想從被掛起的地方繼續(xù)執(zhí)行,就必須知道它上次執(zhí)行到哪個(gè)位置弃理,在JVM中溃论,通過程序計(jì)數(shù)器來記錄某個(gè)線程的字節(jié)碼執(zhí)行位置。因此痘昌,程序計(jì)數(shù)器是具備線程隔離的特性钥勋,也就是說炬转,每個(gè)線程工作時(shí)都有屬于自己的獨(dú)立計(jì)數(shù)器。
2.程序計(jì)數(shù)器的特點(diǎn)
- 線程隔離性算灸,每個(gè)線程工作時(shí)都有屬于自己的獨(dú)立計(jì)數(shù)器返吻,即程序計(jì)數(shù)器是線程私有的
- 執(zhí)行Java方法時(shí),程序計(jì)數(shù)器是有值的乎婿,且記錄的是正在執(zhí)行的字節(jié)碼指令的地址
-
執(zhí)行native本地方法時(shí),程序計(jì)數(shù)器的值為空(Undefined)街佑。因?yàn)閚ative方法是java通過JNI直接調(diào)用本地C/C++庫谢翎,可以近似的認(rèn)為native方法相當(dāng)于C/C++暴露給java的一個(gè)接口,java通過調(diào)用這個(gè)接口從而調(diào)用到C/C++方法沐旨。由于該方法是通過C/C++而不是java進(jìn)行實(shí)現(xiàn)森逮。那么自然無法產(chǎn)生相應(yīng)的字節(jié)碼,并且C/C++執(zhí)行時(shí)的內(nèi)存分配是由自己語言決定的磁携,而不是由JVM決定的褒侧。
- 程序計(jì)數(shù)器占用內(nèi)存很小,在進(jìn)行JVM內(nèi)存計(jì)算時(shí)谊迄,可以忽略不計(jì)闷供。
- 程序計(jì)數(shù)器,是唯一一個(gè)在java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError的區(qū)域统诺。
二歪脏、Java虛擬機(jī)棧(VM Stack)
1、什么是Java虛擬機(jī)棧
- 用于作用于方法執(zhí)行的一塊Java內(nèi)存區(qū)域
- 虛擬機(jī)棧是用于描述java方法執(zhí)行的內(nèi)存模型粮呢。
-
每個(gè)java方法在執(zhí)行時(shí)婿失,會(huì)創(chuàng)建一個(gè)“棧幀(stack frame)”,棧幀的結(jié)構(gòu)分為“局部變量表啄寡、操作數(shù)棧豪硅、動(dòng)態(tài)鏈接、方法出口”幾個(gè)部分(具體的作用會(huì)在字節(jié)碼執(zhí)行引擎章節(jié)中講到挺物,這里只需要了解棧幀是一個(gè)方法執(zhí)行時(shí)所需要數(shù)據(jù)的結(jié)構(gòu))
2懒浮、特點(diǎn)
Java虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同(隨線程而生姻乓,隨線程而滅)
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度嵌溢,將拋出StackOverflowError異常
如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展蹋岩,如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存赖草,就會(huì)拋出OutOfMemoryError異常
Java虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀
對(duì)于我們來說,主要關(guān)注的stack棧內(nèi)存剪个,就是虛擬機(jī)棧中局部變量表部分秧骑。
3、棧幀
- 棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的java虛擬機(jī)棧的棧元素乎折。
- 棧幀存儲(chǔ)了方法的局部變量表绒疗、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息
- 每一個(gè)方法從調(diào)用開始至執(zhí)行完成的過程骂澄,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)里面從入棧到出棧的過程
注意:
在編譯程序代碼的時(shí)候吓蘑,棧幀中需要多大的局部變量表內(nèi)存,多深的操作數(shù)棧都已經(jīng)完全確定了坟冲。
因此一個(gè)棧幀需要分配多少內(nèi)存磨镶,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)健提。
#######棧幀結(jié)構(gòu)如下:
4琳猫、局部變量表
1.局部變量表(Local Variable Table)是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量私痹。并且在Java編譯為Class文件時(shí)脐嫂,就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。
2.局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean紊遵、byte账千、char、short暗膜、int蕊爵、float、long桦山、double)「String是引用類型」攒射,對(duì)象引用(reference類型) 和 returnAddress類型(它指向了一條字節(jié)碼指令的地址)
注意:
很多人說:基本數(shù)據(jù)和對(duì)象引用存儲(chǔ)在棧中。
當(dāng)然這種說法雖然是正確的恒水,但是很不嚴(yán)謹(jǐn)会放,只能說這種說法針對(duì)的是局部變量。
局部變量存儲(chǔ)在局部變量表中钉凌,隨著線程而生咧最,線程而滅。并且線程間數(shù)據(jù)不共享御雕。但是矢沿,如果是成員變量,或者定義在方法外對(duì)象的引用酸纲,它們存儲(chǔ)在堆中捣鲸。
因?yàn)樵诙阎校蔷€程共享數(shù)據(jù)的闽坡,并且棧幀里的命名就已經(jīng)清楚的劃分了界限 : 局部變量表栽惶!
5愁溜、reference(對(duì)象實(shí)例的引用)
個(gè)人感覺和指針類似
一般來說,虛擬機(jī)都能從引用中直接或者間接的查找到對(duì)象的以下兩點(diǎn) :
a.在Java堆中的數(shù)據(jù)存放的起始地址索引外厂。
b.所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)類型冕象。
例如:我們?cè)趧?chuàng)建一個(gè)Student對(duì)象時(shí)的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu):
6、案例
來段代碼試求程序運(yùn)行時(shí)虛擬機(jī)棧的內(nèi)存長度汁蝶,拋出StackOverflowError異常
package yzl.swu.practice;
/** 測(cè)試代碼設(shè)計(jì)思路
* 修改默認(rèn)堆棧大小后,利用遞歸調(diào)用一個(gè)方法,達(dá)到棧深度過大的異常目的,同時(shí)在遞歸調(diào)用過程中記錄調(diào)用此次,得出最大深度的數(shù)據(jù)
* jvm參數(shù)
* -Xss 180k:設(shè)置每個(gè)線程的堆棧大小(最小180k),默認(rèn)是1M
*/
public class TestStackOverflowErrorDemo {
//棧深度統(tǒng)計(jì)值
private int stackLength = 1;
/**
* 遞歸方法,導(dǎo)致棧深度過大異常
*/
public void stackLeak() {
stackLength++;
stackLeak();
}
/**
* 啟動(dòng)方法
* 測(cè)試結(jié)果:當(dāng)-Xss 180k為180k時(shí),stackLength~=1544,隨著-Xss參數(shù)變大時(shí)stackLength值隨之變大
* @param args
*/
public static void main(String[] args) {
TestStackOverflowErrorDemo demo = new TestStackOverflowErrorDemo();
try {
demo.stackLeak();
} catch (Throwable e) {
System.out.println("當(dāng)前棧深度:stackLength=" + demo.stackLength);
e.printStackTrace();
}
}
}
三渐扮、本地方法棧(Native Method Stack)
- 用于作用域本地方法執(zhí)行的一塊Java內(nèi)存區(qū)域
- 本地方法棧的功能和特點(diǎn)類似于虛擬機(jī)棧,均具有線程隔離的特點(diǎn)以及都能拋出StackOverflowError和OutOfMemoryError異常掖棉。
- 不同的是席爽,本地方法棧服務(wù)的對(duì)象是JVM執(zhí)行的native方法(java代碼中使用native關(guān)鍵字標(biāo)記的方法),而虛擬機(jī)棧服務(wù)的是JVM執(zhí)行的java方法啊片。如何去服務(wù)native方法?native方法使用什么語言實(shí)現(xiàn)玖像?怎么組織像棧幀這種為了服務(wù)方法的數(shù)據(jù)結(jié)構(gòu)紫谷?虛擬機(jī)規(guī)范并未給出強(qiáng)制規(guī)定,因此不同的虛擬機(jī)實(shí)可以進(jìn)行自由實(shí)現(xiàn)捐寥,我們常用的HotSpot虛擬機(jī)選擇合并了虛擬機(jī)棧和本地方法棧笤昨。
四、堆(Heap)
1握恳、什么是Java堆
對(duì)于大多數(shù)應(yīng)用來說瞒窒,堆是JVM所管理的內(nèi)存中最大的一塊,也是被所有線程共享的一塊內(nèi)存區(qū)域乡洼,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建崇裁。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存束昵,可謂是對(duì)象的大本營拔稳。此外,堆也是垃圾收集器(GC)管理的主要區(qū)域锹雏。
2巴比、堆的特點(diǎn)
- 一個(gè)jvm實(shí)例只存在一個(gè)堆內(nèi)存,堆也是java內(nèi)存管理的核心區(qū)域
- Java堆區(qū)在JVM啟動(dòng)的時(shí)候即被創(chuàng)建礁遵,其空間大小也就確定了轻绞。是JVM管理的最大一塊內(nèi)存空間(堆內(nèi)存的大小是可以調(diào)節(jié)的)
- 《Java虛擬機(jī)規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中佣耐,但在邏輯上它應(yīng)該被視為連續(xù)的
- 《Java虛擬機(jī)規(guī)范》中對(duì)java堆的描述是:所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在運(yùn)行時(shí)分配在堆上政勃。從實(shí)際使用的角度看,“幾乎”所有的對(duì)象的實(shí)例都在這里分配內(nèi)存
- 數(shù)組或?qū)ο笥肋h(yuǎn)不會(huì)存儲(chǔ)在棧上兼砖,因?yàn)闂斜4嬉眉诓。@個(gè)引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置(String也是引用對(duì)象哦)
- 在方法結(jié)束后选侨,堆中的對(duì)象不會(huì)馬上被移除,僅僅在垃圾收集的時(shí)候才會(huì)被移除
- 堆在邏輯上劃分為“新生代”和“老年代”然走。由于JAVA中的對(duì)象大部分是朝生夕滅援制,還有一小部分能夠長期的駐留在內(nèi)存中,為了對(duì)這兩種對(duì)象進(jìn)行最有效的回收芍瑞,將堆劃分為新生代和老年代晨仑,并且執(zhí)行不同的回收策略。不同的垃圾收集器對(duì)這2個(gè)邏輯區(qū)域的回收機(jī)制不盡相同拆檬。
3洪己、堆的OutOfMemoryError異常
- 當(dāng)堆無法分配對(duì)象內(nèi)存且無法再擴(kuò)展時(shí),會(huì)拋出OutOfMemoryError異常竟贯。
- 一般來說答捕,堆無法分配對(duì)象時(shí)會(huì)進(jìn)行一次GC,如果GC后仍然無法分配對(duì)象屑那,才會(huì)報(bào)內(nèi)存耗盡的錯(cuò)誤拱镐。
代碼測(cè)試一下:
public class Test {
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
//重復(fù)的向list內(nèi)添加1MB大小的數(shù)據(jù),由于list內(nèi)元素不符合GC回收條件進(jìn)而導(dǎo)致OOM持际。
list.add(new byte[1024 * 1024]);
}
}
}
五沃琅、Java方法區(qū)(Method Area)
1、什么是Java方法區(qū)
方法區(qū)蜘欲,也稱非堆(Non-Heap益眉,與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域姥份,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息郭脂、常量、靜態(tài)變量澈歉、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)朱庆。
方法區(qū)結(jié)構(gòu)圖如下:
2、運(yùn)行時(shí)常量池
首先需要知道常量池和運(yùn)行時(shí)常量池的區(qū)別闷祥。
-
常量池
即指class文件常量池娱颊,是class文件的一部分。java文件被編譯成class文件之后凯砍,除了包含了類的版本箱硕、字段、方法悟衩、接口等描述信息剧罩,還有一項(xiàng)信息叫做class文件常量池。其用于存放編譯期生成的各種字面量和符號(hào)引用座泳。 -
運(yùn)行時(shí)常量池
Java語言并不要求常量一定只能在編譯期產(chǎn)生惠昔,運(yùn)行期間也可能產(chǎn)生新的常量幕与,這些常量被放在運(yùn)行時(shí)常量池中。
類加載后镇防,常量池中的數(shù)據(jù)會(huì)在運(yùn)行時(shí)常量池中存放啦鸣!
這里所說的常量包括:基本類型包裝類(包裝類不管理浮點(diǎn)型,整形只會(huì)管理-128到127)和String(也可以通過String.intern()方法可以強(qiáng)制將String放入常量池) -
字符串常量池
HotSpot VM里来氧,記錄interned string的一個(gè)全局表叫做StringTable诫给,它本質(zhì)上就是個(gè)HashSet<String>。注意它只存儲(chǔ)對(duì)java.lang.String實(shí)例的引用啦扬,而不存儲(chǔ)String對(duì)象的內(nèi)容
注意:jdk 1.7后中狂,移除了方法區(qū)間,運(yùn)行時(shí)常量池和字符串常量池都在堆中扑毡。
3胃榕、方法區(qū)的實(shí)現(xiàn)
具體放在哪里,不同的實(shí)現(xiàn)可以放在不同的地方瞄摊。永久代是HotSpot虛擬機(jī)特有的概念勋又,是對(duì)方法區(qū)的實(shí)現(xiàn),別的JVM沒有永久代的概念泉褐。(雖然去除了永久代,但是方法區(qū)作為概念上的區(qū)域仍然存在)
方法區(qū)的實(shí)現(xiàn)鸟蜡,虛擬機(jī)規(guī)范中并未明確規(guī)定膜赃,目前有2種比較主流的實(shí)現(xiàn)方式:
- HotSpot虛擬機(jī)1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作為實(shí)現(xiàn)揉忘,即將GC分代收集擴(kuò)展至方法區(qū)跳座。這種實(shí)現(xiàn)比較偷懶,可以不必為方法區(qū)編寫專門的內(nèi)存管理泣矛,但帶來的后果是容易碰到內(nèi)存溢出的問題(因?yàn)橛谰么?XX:MaxPermSize的上限)疲眷。在JDK1.7+之后,HotSpot逐漸改變方法區(qū)的實(shí)現(xiàn)方式您朽,如1.7版本移除了方法區(qū)中的字符串常量池狂丝。
- HotSpot虛擬機(jī)1.8+:1.8版本中移除了方法區(qū)并使用metaspace(元數(shù)據(jù)空間)作為替代實(shí)現(xiàn)。metaspace占用系統(tǒng)內(nèi)存哗总,也就是說几颜,只要不碰觸到系統(tǒng)內(nèi)存上限,方法區(qū)會(huì)有足夠的內(nèi)存空間讯屈。但這不意味著我們不對(duì)方法區(qū)進(jìn)行限制蛋哭,如果方法區(qū)無限膨脹,最終會(huì)導(dǎo)致系統(tǒng)崩潰涮母。
JDK1.8+ JVM
4谆趾、方法區(qū)的OutOfMemoryError
- 首先躁愿,為什么使用“永久代”并將GC分代收集擴(kuò)展至方法區(qū)這種實(shí)現(xiàn)方式不好,會(huì)導(dǎo)致OOM沪蓬?首先要明白方法區(qū)的內(nèi)存回收目標(biāo)是什么彤钟,方法區(qū)存儲(chǔ)了類的元數(shù)據(jù)信息和各種常量,它的內(nèi)存回收目標(biāo)理應(yīng)當(dāng)是對(duì)這些類型的卸載和常量的回收怜跑。但由于這些數(shù)據(jù)被類的實(shí)例引用样勃,卸載條件變得復(fù)雜且嚴(yán)格,回收不當(dāng)會(huì)導(dǎo)致堆中的類實(shí)例失去元數(shù)據(jù)信息和常量信息性芬。因此峡眶,回收方法區(qū)內(nèi)存不是一件簡(jiǎn)單高效的事情,往往GC在做無用功植锉。另外隨著應(yīng)用規(guī)模的變大辫樱,各種框架的引入,尤其是使用了字節(jié)碼生成技術(shù)的框架俊庇,會(huì)導(dǎo)致方法區(qū)內(nèi)存占用越來越大狮暑,最終OOM。
- 因?yàn)榉椒▍^(qū)最終都會(huì)有一個(gè)最大值上限辉饱,因此若方法區(qū)(含運(yùn)行時(shí)常量池)占用內(nèi)存到達(dá)其最大值搬男,且無法再申請(qǐng)到內(nèi)存時(shí),便會(huì)拋出OutOfMemoryError彭沼。