想搞清楚Java8的內(nèi)存結(jié)構(gòu),看這篇就對了

java8內(nèi)存結(jié)構(gòu)圖

虛擬機(jī)內(nèi)存與本地內(nèi)存的區(qū)別
Java虛擬機(jī)在執(zhí)行的時候會把管理的內(nèi)存分配成不同的區(qū)域,這些區(qū)域被稱為虛擬機(jī)內(nèi)存,同時,對于虛擬機(jī)沒有直接管理的物理內(nèi)存,也有一定的利用,這些被利用卻不在虛擬機(jī)內(nèi)存數(shù)據(jù)區(qū)的內(nèi)存,我們稱它為本地內(nèi)存,這兩種內(nèi)存有一定的區(qū)別:
JVM內(nèi)存
受虛擬機(jī)內(nèi)存大小的參數(shù)控制,當(dāng)大小超過參數(shù)設(shè)置的大小時就會報OOM
本地內(nèi)存
本地內(nèi)存不受虛擬機(jī)內(nèi)存參數(shù)的限制,只受物理內(nèi)存容量的限制 雖然不受參數(shù)的限制,但是如果內(nèi)存的占用超出物理內(nèi)存的大小,同樣也會報OOM
java運(yùn)行時數(shù)據(jù)區(qū)域
java虛擬機(jī)在執(zhí)行過程中會將所管理的內(nèi)存劃分為不同的區(qū)域,有的隨著線程產(chǎn)生和消失,有的隨著java進(jìn)程產(chǎn)生和消失,根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,運(yùn)行時數(shù)據(jù)區(qū)分為以下一個區(qū)域:
程序計數(shù)器(Program Counter Register)
程序計數(shù)器就是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,通過改變計數(shù)器的值,來選取下一行指令,通過他來實現(xiàn)跳轉(zhuǎn)、循環(huán)、恢復(fù)線程等功能。
在任何時刻,一個處理器內(nèi)核只能運(yùn)行一個線程,多線程是通過線程輪流切換,分配時間來完成的,這就需要有一個標(biāo)志來記住每個線程執(zhí)行到了哪里,這里便需要到了程序計數(shù)器。 所以,程序計數(shù)器是線程私有的,每個線程都已自己的程序計數(shù)器。
虛擬機(jī)棧(JVM Stacks)

虛擬機(jī)棧是線程私有的,隨線程生滅。虛擬機(jī)棧描述的是線程中的方法的內(nèi)存模型:
每個方法被執(zhí)行的時候,都會在虛擬機(jī)棧中同步創(chuàng)建一個棧幀(stack frame)。
每個棧幀的包含如下的內(nèi)容
局部變量表 局部變量表中存儲著方法里的java基本數(shù)據(jù)類型(byte/boolean/char/int/long/double/float/short)以及對象的引用(注:這里的基本數(shù)據(jù)類型指的是方法內(nèi)的局部變量) 操作數(shù)棧 動態(tài)連接 方法返回地址
方法被執(zhí)行時入棧,執(zhí)行完后出棧
虛擬機(jī)棧可能會拋出兩種異常:
如果線程請求的棧深度大于虛擬機(jī)所規(guī)定的棧深度,則會拋出StackOverFlowError即棧溢出 如果虛擬機(jī)的棧容量可以動態(tài)擴(kuò)展,那么當(dāng)虛擬機(jī)棧申請不到內(nèi)存時會拋出OutOfMemoryError即OOM內(nèi)存溢出
本地方法棧(Native Method Stacks)
本地方法棧與虛擬機(jī)棧的作用是相似的,都會拋出OutOfMemoryError和StackOverFlowError,都是線程私有的,主要的區(qū)別在于:
虛擬機(jī)棧執(zhí)行的是java方法 本地方法棧執(zhí)行的是native方法(什么是Native方法?)
Java堆(Java Heap)
java堆是JVM內(nèi)存中最大的一塊,由所有線程共享,是由垃圾收集器管理的內(nèi)存區(qū)域,主要存放對象實例,當(dāng)然由于java虛擬機(jī)的發(fā)展,堆中也多了許多東西,現(xiàn)在主要有:
對象實例 類初始化生成的對象 基本數(shù)據(jù)類型的數(shù)組也是對象實例 字符串常量池 字符串常量池原本存放于方法區(qū),jdk7開始放置于堆中。 字符串常量池存儲的是string對象的直接引用,而不是直接存放的對象,是一張string table 靜態(tài)變量 靜態(tài)變量是有static修飾的變量,jdk7時從方法區(qū)遷移至堆中 線程分配緩沖區(qū)(Thread Local Allocation Buffer) 線程私有,但是不影響java堆的共性 增加線程分配緩沖區(qū)是為了提升對象分配時的效率
java堆既可以是固定大小的,也可以是可擴(kuò)展的(通過參數(shù)-Xmx和-Xms設(shè)定),如果堆無法擴(kuò)展或者無法分配內(nèi)存時也會報OOM。
方法區(qū)(Method Area)
方法區(qū)絕對是網(wǎng)上所有關(guān)于java內(nèi)存結(jié)構(gòu)文章爭論的焦點,因為方法區(qū)的實現(xiàn)在java8做了一次大革新,現(xiàn)在我們來討論一下:
方法區(qū)是所有線程共享的內(nèi)存,在java8以前是放在JVM內(nèi)存中的,由永久代實現(xiàn),受JVM內(nèi)存大小參數(shù)的限制,在java8中移除了永久代的內(nèi)容,方法區(qū)由元空間(Meta Space)實現(xiàn),并直接放到了本地內(nèi)存中,不受JVM參數(shù)的限制(當(dāng)然,如果物理內(nèi)存被占滿了,方法區(qū)也會報OOM),并且將原來放在方法區(qū)的字符串常量池和靜態(tài)變量都轉(zhuǎn)移到了Java堆中,方法區(qū)與其他區(qū)域不同的地方在于,方法區(qū)在編譯期間和類加載完成后的內(nèi)容有少許不同,不過總的來說分為這兩部分:
類元信息(Klass)
類元信息在類編譯期間放入方法區(qū),里面放置了類的基本信息,包括類的版本、字段、方法、接口以及常量池表(Constant Pool Table) 常量池表(Constant Pool Table)存儲了類在編譯期間生成的字面量、符號引用(什么是字面量?什么是符號引用?),這些信息在類加載完后會被解析到運(yùn)行時常量池中
運(yùn)行時常量池(Runtime Constant Pool)
運(yùn)行時常量池主要存放在類加載后被解析的字面量與符號引用,但不止這些 運(yùn)行時常量池具備動態(tài)性,可以添加數(shù)據(jù),比較多的使用就是String類的intern()方法
直接內(nèi)存
直接內(nèi)存位于本地內(nèi)存,不屬于JVM內(nèi)存,但是也會在物理內(nèi)存耗盡的時候報OOM,所以也講一下。
在jdk1.4中加入了NIO(New Input/Putput)類,引入了一種基于通道(channel)與緩沖區(qū)(buffer)的新IO方式,它可以使用native函數(shù)直接分配堆外內(nèi)存,然后通過存儲在java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作,這樣可以在一些場景下大大提高IO性能,避免了在java堆和native堆來回復(fù)制數(shù)據(jù)。
常見問題
什么是Native方法?
由于java是一門高級語言,離硬件底層比較遠(yuǎn),有時候無法操作底層的資源,于是,java添加了native關(guān)鍵字,被native關(guān)鍵字修飾的方法可以用其他語言重寫,這樣,我們就可以寫一個本地方法,然后用C語言重寫,這樣來操作底層資源。當(dāng)然,使用了native方法會導(dǎo)致系統(tǒng)的可移植性不高,這是需要注意的。
成員變量、局部變量、類變量分別存儲在內(nèi)存的什么地方?
類變量
類變量是用static修飾符修飾,定義在方法外的變量,隨著java進(jìn)程產(chǎn)生和銷毀 在java8之前把靜態(tài)變量存放于方法區(qū),在java8時存放在堆中
成員變量
成員變量是定義在類中,但是沒有static修飾符修飾的變量,隨著類的實例產(chǎn)生和銷毀,是類實例的一部分 由于是實例的一部分,在類初始化的時候,從運(yùn)行時常量池取出直接引用或者值,與初始化的對象一起放入堆中
局部變量
局部變量是定義在類的方法中的變量 在所在方法被調(diào)用時放入虛擬機(jī)棧的棧幀中,方法執(zhí)行結(jié)束后從虛擬機(jī)棧中彈出,所以存放在虛擬機(jī)棧中
由final修飾的常量存放在哪里?
final關(guān)鍵字并不影響在內(nèi)存中的位置,具體位置請參考上一問題。
類常量池、運(yùn)行時常量池、字符串常量池有什么關(guān)系?有什么區(qū)別?
類常量池與運(yùn)行時常量池都存儲在方法區(qū),而字符串常量池在jdk7時就已經(jīng)從方法區(qū)遷移到了java堆中。
在類編譯過程中,會把類元信息放到方法區(qū),類元信息的其中一部分便是類常量池,主要存放字面量和符號引用,而字面量的一部分便是文本字符,在類加載時將字面量和符號引用解析為直接引用存儲在運(yùn)行時常量池;
對于文本字符來說,它們會在解析時查找字符串常量池,查出這個文本字符對應(yīng)的字符串對象的直接引用,將直接引用存儲在運(yùn)行時常量池;字符串常量池存儲的是字符串對象的引用,而不是字符串本身。
什么是字面量?什么是符號引用?
字面量
java代碼在編譯過程中是無法構(gòu)建引用的,字面量就是在編譯時對于數(shù)據(jù)的一種表示:
int a=1;//這個1便是字面量
String b="iloveu";//iloveu便是字面量
符號引用
由于在編譯過程中并不知道每個類的地址,因為可能這個類還沒有加載,所以如果你在一個類中引用了另一個類,那么你完全無法知道他的內(nèi)存地址,那怎么辦,我們只能用他的類名作為符號引用,在類加載完后用這個符號引用去獲取他的內(nèi)存地址。
例子:我在com.demo.Solution類中引用了com.test.Quest,那么我會把com.test.Quest作為符號引用存到類常量池,等類加載完后,拿著這個引用去方法區(qū)找這個類的內(nèi)存地址。
