本文主要對JVM 內(nèi)存結(jié)構(gòu)進行講解,注意不要和Java內(nèi)存模型混淆了。
內(nèi)存結(jié)構(gòu)是指 Jvm 運行時將數(shù)據(jù)分區(qū)域存儲,強調(diào)對內(nèi)存空間的劃分。
內(nèi)存模型(Java Memory Model,簡稱 JMM )是定義了線程和主內(nèi)存之間的抽象關(guān)系,即 JMM 定義了 JVM 在計算機內(nèi)存(RAM)中的工作方式,是虛擬機的內(nèi)存管理模型,是一種虛擬機工程規(guī)范。
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和 CPU 的中間倉庫及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實時運行。JVM 內(nèi)存布局規(guī)定了 Java 在運行過程中內(nèi)存申請、分配、管理的策略,保證了 JVM 的高效穩(wěn)定運行。不同的 JVM 對于內(nèi)存的劃分方式和管理機制存在著部分差異。
下圖是 JVM 整體架構(gòu),中間部分就是 Java 虛擬機定義的各種運行時數(shù)據(jù)區(qū)域。
Java 虛擬機(源于網(wǎng)絡(luò))
Java 虛擬機定義了若干種程序運行期間會使用到的運行時數(shù)據(jù)區(qū),其中有一些會隨著虛擬機啟動而創(chuàng)建,隨著虛擬機退出而銷毀。另外一些則是與線程一一對應(yīng)的,這些與線程一一對應(yīng)的數(shù)據(jù)區(qū)域會隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現(xiàn)),字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。
PC 寄存器用來存儲指向下一條指令的地址(IDEA 插件 Jclasslib查看字節(jié)碼)
由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間的計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是Natvie方法,這個計數(shù)器值則為空(Undefined)。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
因為CPU需要不停的切換各個線程,這時候切換回來以后,就得知道接著從哪開始繼續(xù)執(zhí)行。JVM的字節(jié)碼解釋器就需要通過改變PC寄存器的值來明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令。
多線程在一個特定的時間段內(nèi)只會執(zhí)行其中某一個線程方法,CPU會不停的做任務(wù)切換,這樣必然會導(dǎo)致經(jīng)常中斷或恢復(fù)。為了能夠準確的記錄各個線程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址,所以為每個線程都分配了一個PC寄存器,每個線程都獨立計算,不會互相影響。
與程序計數(shù)器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
棧幀的內(nèi)部結(jié)構(gòu)
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,根據(jù)不同的虛擬機實現(xiàn),它可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苤赶蛞粋€代表對象的句柄或者其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
操作數(shù)棧,和局部變量區(qū)一樣,也被組織成一個以字長為單位的數(shù)組,但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的,可把操作數(shù)棧理解為存儲計算時,臨時數(shù)據(jù)的存儲區(qū)域。
除了局部變量區(qū)和操作數(shù)棧外,java棧幀還需要一些數(shù)據(jù)來支持常量池解析、正常方法返回以及異常派發(fā)機制。這些數(shù)據(jù)都保存在java棧幀的幀數(shù)據(jù)區(qū)中。
在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態(tài)擴展(當(dāng)前大部分的Java虛擬機都可動態(tài)擴展,只不過Java虛擬機規(guī)范中也允許固定長度的虛擬機棧),當(dāng)擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError異常。
可以通過參數(shù)-Xss來設(shè)置每個線程的棧大小,棧的大小直接決定了函數(shù)調(dià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 時候,可以在 debug 窗口看到 Frames 中各種方法的壓棧和出棧情況。
本地方法棧(Native Method Stacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native方法服務(wù)。具體做法是 Native Method Stack 中登記 native 方法,在 Execution Engine 執(zhí)行時加載本地方法庫當(dāng)某個線程調(diào)用一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界。它和虛擬機擁有同樣的權(quán)限。
虛擬機規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它(并不是所有 JVM 都支持本地方法。如果 JVM 產(chǎn)品不打算支持 native 方法,也可以無需實現(xiàn)本地方法棧)。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
棧是運行時的單位,而堆是存儲的單位。
棧解決程序的運行問題,即程序如何執(zhí)行,或者說如何處理數(shù)據(jù)。堆解決的是數(shù)據(jù)存儲的問題,即數(shù)據(jù)怎么放、放在哪。
對于大多數(shù)應(yīng)用,Java 堆是 Java 虛擬機管理的內(nèi)存中最大的一塊,被所有線程共享。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數(shù)據(jù)都在這里分配內(nèi)存。
為什么“幾乎所有的對象實例都在這里分配內(nèi)存,而不是全部”?
為了進行高效的垃圾回收,虛擬機把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
年輕代和老年代的劃分是為了更好的內(nèi)存分派及回收(源于網(wǎng)絡(luò))
年輕代是所有新對象創(chuàng)建的地方。當(dāng)填充年輕代時,執(zhí)行垃圾收集。這種垃圾收集稱為 Minor GC。年輕一代被分為三個部分——伊甸園(Eden Memory)和兩個幸存區(qū)(Survivor Memory,被稱為from/to或s0/s1),默認比例是8:1:1
老年代中存放的對象是存活了很久的,年齡大于15的對象。在老年代觸發(fā)的gc叫major gc也叫full gc。full gc會包含年輕代的gc。但老年代只要執(zhí)行g(shù)c就一定是full gc。在執(zhí)行full gc的情況下,會阻塞程序的正常運行。老年代的gc比年輕代的gc效率上慢10倍以上,對效率有很大的影響。
大對象直接進入老年代(大對象是指需要大量連續(xù)內(nèi)存空間的對象)。這樣做的目的是避免在 Eden 區(qū)和兩個Survivor 區(qū)之間發(fā)生大量的內(nèi)存拷貝。
java8前后
?? 持久代在物理層面,是在堆空間的;而在邏輯層面,是在方法區(qū)的;因為方法區(qū)其實物理上也是在堆中的,但是由于功能和作用的區(qū)別,邏輯上方法區(qū)是獨立于堆的。
元空間是metaspace,在jdk1.8的時候,jvm移除了永久代的概念,元空間也是對java虛擬機的方法區(qū)的一種實現(xiàn)。元空間與永久代最大的區(qū)別在于,元空間不在虛擬機中,使用本地內(nèi)存。
永久代的回收會隨著full gc進行移動,消耗性能。每種類型的垃圾回收都需要特殊處理元數(shù)據(jù)。將元數(shù)據(jù)剝離出來,簡化了垃圾收集,提高了效率。
Java 虛擬機規(guī)范規(guī)定,Java 堆可以是處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,像磁盤空間一樣。實現(xiàn)時,既可以是固定大小,也可以是可擴展的,主流虛擬機都是可擴展的(通過 -Xmx 和 -Xms 控制),如果堆中沒有完成實例分配,并且堆無法再擴展時,就會拋出 OutOfMemoryError 異常。
為對象分配內(nèi)存是一件非常嚴謹和復(fù)雜的任務(wù),JVM 的設(shè)計者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題,并且由于內(nèi)存分配算法和內(nèi)存回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存回收后是否會在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
方法區(qū)用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。
對每個加載的類型(類 class、接口 interface、枚舉 enum、注解 annotation),JVM 必須在方法區(qū)中存儲以下類型信息
JVM 必須保存所有方法的
jdk1.6及之前 | 有永久代,靜態(tài)變量存放在永久代上 |
jdk1.7 | 有永久代,但已經(jīng)逐步“去永久代”,字符串常量池、靜態(tài)變量移除,保存在堆中 |
jdk1.8及之后 | 取消永久代,類型信息、字段、方法、常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍在堆中 |
只有 HotSpot 才有永久代的概念
方法區(qū)和永久代以及元空間的關(guān)系很像(實際不是)Java 中接口和類的關(guān)系,類實現(xiàn)了接口,這里的類就可以看作是永久代和元空間,接口可以看作是方法區(qū),也就是說永久代以及元空間是 HotSpot 虛擬機對虛擬機規(guī)范中方法區(qū)的兩種實現(xiàn)方式。
關(guān)系示意圖
本文鏈接:http://www.www897cc.com/showinfo-26-17175-0.htmlJVM 內(nèi)存結(jié)構(gòu)詳解,看這一篇就夠了
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 什么是API管理?你了解嗎?
下一篇: 如何成為前1%的程序員