JVM 內(nèi)存結(jié)構(gòu)詳解,看這一篇就夠了
本文主要對(duì)JVM 內(nèi)存結(jié)構(gòu)進(jìn)行講解,注意不要和Java內(nèi)存模型混淆了。
內(nèi)存結(jié)構(gòu)是指 Jvm 運(yùn)行時(shí)將數(shù)據(jù)分區(qū)域存儲(chǔ),強(qiáng)調(diào)對(duì)內(nèi)存空間的劃分。
內(nèi)存模型(Java Memory Model,簡(jiǎn)稱 JMM )是定義了線程和主內(nèi)存之間的抽象關(guān)系,即 JMM 定義了 JVM 在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式,是虛擬機(jī)的內(nèi)存管理模型,是一種虛擬機(jī)工程規(guī)范。
運(yùn)行時(shí)數(shù)據(jù)區(qū)
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和 CPU 的中間倉(cāng)庫(kù)及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行。JVM 內(nèi)存布局規(guī)定了 Java 在運(yùn)行過程中內(nèi)存申請(qǐng)、分配、管理的策略,保證了 JVM 的高效穩(wěn)定運(yùn)行。不同的 JVM 對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異。
下圖是 JVM 整體架構(gòu),中間部分就是 Java 虛擬機(jī)定義的各種運(yùn)行時(shí)數(shù)據(jù)區(qū)域。
Java 虛擬機(jī)(源于網(wǎng)絡(luò))
Java 虛擬機(jī)定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū),其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷毀。另外一些則是與線程一一對(duì)應(yīng)的,這些與線程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
- 線程私有:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)。
- 線程共享:堆、方法區(qū), 堆外內(nèi)存(Java7的永久代或JDK8的元空間、代碼緩存)。
程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里(僅是概念模型,各種虛擬機(jī)可能會(huì)通過一些更高效的方式去實(shí)現(xiàn)),字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
PC 寄存器用來存儲(chǔ)指向下一條指令的地址(IDEA 插件 Jclasslib查看字節(jié)碼)
由于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)椤?/span>線程私有”的內(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ū)域。
- 使用程序計(jì)數(shù)器(PC寄存器)存儲(chǔ)字節(jié)碼指令地址有什么用呢?為什么使用程序計(jì)數(shù)器記錄當(dāng)前線程的執(zhí)行地址呢?
因?yàn)镃PU需要不停的切換各個(gè)線程,這時(shí)候切換回來以后,就得知道接著從哪開始繼續(xù)執(zhí)行。JVM的字節(jié)碼解釋器就需要通過改變PC寄存器的值來明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令。
- 程序計(jì)數(shù)器(PC寄存器)為什么會(huì)被設(shè)定為線程私有的?
多線程在一個(gè)特定的時(shí)間段內(nèi)只會(huì)執(zhí)行其中某一個(gè)線程方法,CPU會(huì)不停的做任務(wù)切換,這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù)。為了能夠準(zhǔn)確的記錄各個(gè)線程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址,所以為每個(gè)線程都分配了一個(gè)PC寄存器,每個(gè)線程都獨(dú)立計(jì)算,不會(huì)互相影響。
JVM線程棧幀
(1)什么是虛擬機(jī)棧
與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
棧幀的內(nèi)部結(jié)構(gòu)
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它不等同于對(duì)象本身,根據(jù)不同的虛擬機(jī)實(shí)現(xiàn),它可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能指向一個(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
操作數(shù)棧,和局部變量區(qū)一樣,也被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組,但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的,可把操作數(shù)棧理解為存儲(chǔ)計(jì)算時(shí),臨時(shí)數(shù)據(jù)的存儲(chǔ)區(qū)域。
除了局部變量區(qū)和操作數(shù)棧外,java棧幀還需要一些數(shù)據(jù)來支持常量池解析、正常方法返回以及異常派發(fā)機(jī)制。這些數(shù)據(jù)都保存在java棧幀的幀數(shù)據(jù)區(qū)中。
在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),當(dāng)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。
可以通過參數(shù)-Xss來設(shè)置每個(gè)線程的棧大小,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度。
(2)棧運(yùn)行原理
- JVM 直接對(duì) Java 棧的操作只有兩個(gè),對(duì)棧幀的壓棧和出棧,遵循“先進(jìn)后出/后進(jìn)先出”原則。
- 在一條活動(dòng)線程中,一個(gè)時(shí)間點(diǎn)上,只會(huì)有一個(gè)活動(dòng)的棧幀。即只有當(dāng)前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的,這個(gè)棧幀被稱為當(dāng)前棧幀(Current Frame),與當(dāng)前棧幀對(duì)應(yīng)的方法就是當(dāng)前方法(Current Method),定義這個(gè)方法的類就是當(dāng)前類(Current Class)。
- 執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只針對(duì)當(dāng)前棧幀進(jìn)行操作。
- 如果在該方法中調(diào)用了其他方法,對(duì)應(yīng)的新的棧幀會(huì)被創(chuàng)建出來,放在棧的頂端,稱為新的當(dāng)前棧幀。
- 不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個(gè)棧幀中引用另外一個(gè)線程的棧幀。
- 如果當(dāng)前方法調(diào)用了其他方法,方法返回之際,當(dāng)前棧幀會(huì)傳回此方法的執(zhí)行結(jié)果給前一個(gè)棧幀,接著,虛擬機(jī)會(huì)丟棄當(dāng)前棧幀,使得前一個(gè)棧幀重新成為當(dāng)前棧幀。
- Java 方法有兩種返回函數(shù)的方式,一種是正常的函數(shù)返回,使用 return 指令,另一種是拋出異常,不管用哪種方式,都會(huì)導(dǎo)致棧幀被彈出。
public class StackFrameTest {
public static void main(String[] args) {
test1();
}
private static void test1(){
System.out.println("test1 start");
test2();
System.out.println("test1 end");
}
private static void test2(){
System.out.println("test1 start");
test3();
System.out.println("test1 end");
}
private static int test3(){
System.out.println("test1 start");
int i = 1;
System.out.println("test1 end");
return i;
}
}
IDEA 在 debug 時(shí)候,可以在 debug 窗口看到 Frames 中各種方法的壓棧和出棧情況。
本地方法棧(Native Method Stacks)
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)。具體做法是 Native Method Stack 中登記 native 方法,在 Execution Engine 執(zhí)行時(shí)加載本地方法庫(kù)當(dāng)某個(gè)線程調(diào)用一個(gè)本地方法時(shí),它就進(jìn)入了一個(gè)全新的并且不再受虛擬機(jī)限制的世界。它和虛擬機(jī)擁有同樣的權(quán)限。
虛擬機(jī)規(guī)范中對(duì)本地方法棧中的方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它(并不是所有 JVM 都支持本地方法。如果 JVM 產(chǎn)品不打算支持 native 方法,也可以無需實(shí)現(xiàn)本地方法棧)。甚至有的虛擬機(jī)(譬如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常。
棧是運(yùn)行時(shí)的單位,而堆是存儲(chǔ)的單位。
棧解決程序的運(yùn)行問題,即程序如何執(zhí)行,或者說如何處理數(shù)據(jù)。堆解決的是數(shù)據(jù)存儲(chǔ)的問題,即數(shù)據(jù)怎么放、放在哪。
堆內(nèi)存
(1)內(nèi)存劃分
對(duì)于大多數(shù)應(yīng)用,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊,被所有線程共享。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存。
為什么“幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,而不是全部”?
為了進(jìn)行高效的垃圾回收,虛擬機(jī)把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
- 新生代(年輕代):新對(duì)象和沒達(dá)到一定年齡的對(duì)象都在新生代。
- 老年代(養(yǎng)老區(qū)):被長(zhǎng)時(shí)間使用的對(duì)象,老年代的內(nèi)存空間應(yīng)該要比年輕代更大。
- 元空間(JDK1.8 之前叫永久代):像一些方法中的操作臨時(shí)對(duì)象等,JDK1.8 之前是占用 JVM 內(nèi)存,JDK1.8 之后直接使用物理內(nèi)存。
年輕代和老年代的劃分是為了更好的內(nèi)存分派及回收(源于網(wǎng)絡(luò))
- 年輕代 (Young Generation)
年輕代是所有新對(duì)象創(chuàng)建的地方。當(dāng)填充年輕代時(shí),執(zhí)行垃圾收集。這種垃圾收集稱為 Minor GC。年輕一代被分為三個(gè)部分——伊甸園(Eden Memory)和兩個(gè)幸存區(qū)(Survivor Memory,被稱為from/to或s0/s1),默認(rèn)比例是8:1:1
- 大多數(shù)新創(chuàng)建的對(duì)象都位于 Eden 內(nèi)存空間中。
- 當(dāng) Eden 空間被對(duì)象填充時(shí),執(zhí)行Minor GC,并將所有幸存者對(duì)象移動(dòng)到一個(gè)幸存者空間中。
- Minor GC 檢查幸存者對(duì)象,并將它們移動(dòng)到另一個(gè)幸存者空間。所以總有一個(gè)幸存者空間是空的。
- 經(jīng)過多次 GC 循環(huán)后存活下來的對(duì)象被移動(dòng)到老年代。通常,這是通過設(shè)置年輕一代對(duì)象的年齡閾值來實(shí)現(xiàn)的,然后他們才有資格提升到老一代。
- 老年代(Old Generation)。
老年代中存放的對(duì)象是存活了很久的,年齡大于15的對(duì)象。在老年代觸發(fā)的gc叫major gc也叫full gc。full gc會(huì)包含年輕代的gc。但老年代只要執(zhí)行g(shù)c就一定是full gc。在執(zhí)行full gc的情況下,會(huì)阻塞程序的正常運(yùn)行。老年代的gc比年輕代的gc效率上慢10倍以上,對(duì)效率有很大的影響。
大對(duì)象直接進(jìn)入老年代(大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象)。這樣做的目的是避免在 Eden 區(qū)和兩個(gè)Survivor 區(qū)之間發(fā)生大量的內(nèi)存拷貝。
java8前后
?? 持久代在物理層面,是在堆空間的;而在邏輯層面,是在方法區(qū)的;因?yàn)榉椒▍^(qū)其實(shí)物理上也是在堆中的,但是由于功能和作用的區(qū)別,邏輯上方法區(qū)是獨(dú)立于堆的。
- 元空間
元空間是metaspace,在jdk1.8的時(shí)候,jvm移除了永久代的概念,元空間也是對(duì)java虛擬機(jī)的方法區(qū)的一種實(shí)現(xiàn)。元空間與永久代最大的區(qū)別在于,元空間不在虛擬機(jī)中,使用本地內(nèi)存。
永久代的回收會(huì)隨著full gc進(jìn)行移動(dòng),消耗性能。每種類型的垃圾回收都需要特殊處理元數(shù)據(jù)。將元數(shù)據(jù)剝離出來,簡(jiǎn)化了垃圾收集,提高了效率。
Java 虛擬機(jī)規(guī)范規(guī)定,Java 堆可以是處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,像磁盤空間一樣。實(shí)現(xiàn)時(shí),既可以是固定大小,也可以是可擴(kuò)展的,主流虛擬機(jī)都是可擴(kuò)展的(通過 -Xmx 和 -Xms 控制),如果堆中沒有完成實(shí)例分配,并且堆無法再擴(kuò)展時(shí),就會(huì)拋出 OutOfMemoryError 異常。
(2)對(duì)象的分配過程
為對(duì)象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題,并且由于內(nèi)存分配算法和內(nèi)存回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存回收后是否會(huì)在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
- new 的對(duì)象先放在伊甸園區(qū),此區(qū)有大小限制。
- 當(dāng)伊甸園的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,JVM 的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀。再加載新的對(duì)象放到伊甸園區(qū)
- 然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者 0 區(qū)
- 如果再次觸發(fā)垃圾回收,此時(shí)上次幸存下來的放到幸存者 0 區(qū),如果沒有回收,就會(huì)放到幸存者 1 區(qū)
- 如果再次經(jīng)歷垃圾回收,此時(shí)會(huì)重新放回幸存者 0 區(qū),接著再去幸存者 1 區(qū)
- 什么時(shí)候才會(huì)去養(yǎng)老區(qū)呢? 默認(rèn)是 15 次回收標(biāo)記
- 在養(yǎng)老區(qū),相對(duì)悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),再次觸發(fā) Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理
- 若養(yǎng)老區(qū)執(zhí)行了 Major GC 之后發(fā)現(xiàn)依然無法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生 OOM 異常
五、方法區(qū)
- 方法區(qū)(Method Area)與 Java 堆一樣,是所有線程共享的內(nèi)存區(qū)域。
- 雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開。
- 運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將類在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的是 String.intern()方法。受方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。
- 方法區(qū)的大小和堆空間一樣,可以選擇固定大小也可選擇可擴(kuò)展,方法區(qū)的大小決定了系統(tǒng)可以放多少個(gè)類,如果系統(tǒng)類太多,導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤
- JVM 關(guān)閉后方法區(qū)即被釋放
(1)方法區(qū)內(nèi)部結(jié)構(gòu)
方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等。
類型信息
對(duì)每個(gè)加載的類型(類 class、接口 interface、枚舉 enum、注解 annotation),JVM 必須在方法區(qū)中存儲(chǔ)以下類型信息
- 這個(gè)類型的完整有效名稱(全名=包名.類名)
- 這個(gè)類型直接父類的完整有效名(對(duì)于 interface或是 java.lang.Object,都沒有父類)
- 這個(gè)類型的修飾符(public,abstract,final 的某個(gè)子集)
- 這個(gè)類型直接接口的一個(gè)有序列表
域(Field)信息
- JVM 必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序
- 域的相關(guān)信息包括:域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient 的某個(gè)子集)
方法(Method)信息
JVM 必須保存所有方法的
- 方法名稱
- 方法的返回類型
- 方法參數(shù)的數(shù)量和類型
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract 的一個(gè)子集)
- 方法的字符碼(bytecodes)、操作數(shù)棧、局部變量表及大?。╝bstract 和 native 方法除外)
- 異常表(abstract 和 native 方法除外)每個(gè)異常處理的開始位置、結(jié)束位置、代碼處理在程序計(jì)數(shù)器中的偏移地址、被捕獲的異常類的常量池索引
(2)方法區(qū)在 JDK6、7、8中的演進(jìn)細(xì)節(jié)
jdk1.6及之前 | 有永久代,靜態(tài)變量存放在永久代上 |
jdk1.7 | 有永久代,但已經(jīng)逐步“去永久代”,字符串常量池、靜態(tài)變量移除,保存在堆中 |
jdk1.8及之后 | 取消永久代,類型信息、字段、方法、常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍在堆中 |
只有 HotSpot 才有永久代的概念
(3)方法區(qū)和永久代以及元空間有什么關(guān)系
方法區(qū)和永久代以及元空間的關(guān)系很像(實(shí)際不是)Java 中接口和類的關(guān)系,類實(shí)現(xiàn)了接口,這里的類就可以看作是永久代和元空間,接口可以看作是方法區(qū),也就是說永久代以及元空間是 HotSpot 虛擬機(jī)對(duì)虛擬機(jī)規(guī)范中方法區(qū)的兩種實(shí)現(xiàn)方式。
關(guān)系示意圖