java8的內(nèi)存結(jié)構(gòu),這一篇文章就夠了

在一開始學(xué)習(xí)java的時候,那時候是在網(wǎng)上看視頻,老師就經(jīng)常提到什么對象分配在堆區(qū),什么在棧區(qū),那時候和理解,后來理解了就想著寫一篇文章好好的去梳理一下。

想說一下這篇文章的脈絡(luò):

首先,研究java7的內(nèi)存結(jié)構(gòu),并對其進行一個詳細的介紹,因為理解了java7之后java8比較容易理解

接下來,使用一個例子來詳解我們在運行一個程序的時候,代碼在java虛擬機中的存儲和轉(zhuǎn)化。

最后,我們給出java8的內(nèi)存結(jié)構(gòu),看一看做了哪些改動,并和java7進行一個比較。

第一部分:java7內(nèi)存結(jié)構(gòu)

先給一張java7的內(nèi)存結(jié)構(gòu)圖吧(我用Windows里面的畫圖工具畫的,所以看起來不怎么美觀)

首先對這個圖有一個認識,從上面可以看到j(luò)ava7的內(nèi)存結(jié)構(gòu)大致分了五個部分:PC寄存器,java虛擬機棧、本地方法棧、java堆、方法區(qū)。其中PC寄存器、java虛擬機棧和本地方法棧是所有線程共享的一塊內(nèi)存區(qū)域。java堆和方法區(qū)是每一個線程隔離的一塊區(qū)域,其中,方法區(qū)還有一個運行時常量池。

接下來看一看每一塊區(qū)域里面存放的什么?

一、PC寄存器

在大學(xué)的時候?qū)W過計算機組成原理的時候都知道,內(nèi)存里面有很多寄存器,大概幾百個吧(目前的,之前大學(xué)學(xué)的時候老師說才幾十個),每一種寄存器的用途都不一樣,其中有一個寄存器就是程序計數(shù)器。這個寄存器的主要作用就是存放下一條需要執(zhí)行的指令。

首先,為什么要有這個程序計數(shù)器呢?這是因為我們的處理器在一個時刻,只能執(zhí)行一個線程中的指令。但是我們的程序往往都是多線程的,這時候處理器就需要來回切換我們的線程,為了在線程切換之后回到之前正確的位置上,此時就需要一個程序計數(shù)器,這也就很容易理解了我們的每個線程都有一個自己的程序計數(shù)器來保存自己之前的狀態(tài)。

接下來如何理解這個程序計數(shù)器的功能呢?假如我們的程序代碼假如是一行一行執(zhí)行的,程序計數(shù)器永遠指向下一行需要執(zhí)行的字節(jié)碼指令。在循環(huán)結(jié)構(gòu)中,我們就可以改變程序計數(shù)器中的值,來指向下一條需要執(zhí)行的指令。因此,在分支、循環(huán)、跳轉(zhuǎn)、異常處理和線程恢復(fù)等等一些場景都需要這個程序計數(shù)器來完成。

最后看一下在什么情況下,應(yīng)該存儲什么內(nèi)容?!秊ava虛擬機規(guī)范》中說如果當前執(zhí)行的是 Java 的方法,則該寄存器中保存當前執(zhí)行指令的地址;倘若執(zhí)行的是native 方法,則PC寄存器中為空(Undefined)。PC寄存區(qū)區(qū)域就是存放了N多個這樣的寄存區(qū)。此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。因此可以把他的幾個特點歸納如下。

程序計數(shù)器指定下一條需要執(zhí)行的指令

每一個線程獨有一個程序計數(shù)器

執(zhí)行java代碼時,寄存器保存當前指令地址

執(zhí)行native方法時候,寄存器為空。

不會造成OutOfMemoryError情況

二、Java虛擬機棧

每一個線程都有自己的java虛擬機棧,這個棧與線程同時創(chuàng)建,一個線程中的每個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。每個線程有一個私有的棧,隨著線程的創(chuàng)建而創(chuàng)建。棧里面存著的是一種叫“棧幀”的東西,每個方法會創(chuàng)建一個棧幀,棧幀中存放了局部變量表(基本數(shù)據(jù)類型和對象引用)、操作數(shù)棧、動態(tài)連接和返回地址等信息。當前運行方法對應(yīng)的棧幀叫做當前棧幀。下面主要對這個棧幀進行一個介紹。

先看一張圖

首先,局部變量表里存放了編譯期間可知的各種基本數(shù)據(jù)類型(8種)、對象引用、returnAddress類型(指向一條字節(jié)碼指令的地址)。他有如下特點:

64位長度的long和double類型占用2個局部變量空間(Slot),其余數(shù)據(jù)類型只占用一個。

局部變量表所需的內(nèi)存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,

在方法運行期間不會改變局部變量表的大小。

接下來操作數(shù)棧,其實在棧幀剛剛創(chuàng)建的時候,操作數(shù)棧是空的,java虛擬機可以從局部變量表或者對象的實例字段中,復(fù)制一些常量或者變量值到操作數(shù)棧中。也可以從操作數(shù)棧中取走數(shù)據(jù)。他的深度在編譯期就已經(jīng)確定了。

動態(tài)連接是什么意思呢?在這里我們先有個基本的印象,下面舉例子的時候,再來看這個解釋比較容易理解一點,我們知道,在線程中一個方法去調(diào)用另外一個方法,是通過符號引用來實現(xiàn)的,動態(tài)連接的作用就是把這個符號引用表示的方法轉(zhuǎn)化為實際方法的直接引用。

對于java虛擬機棧的描述,最后看一下可能發(fā)生的異常情況:

如果線程請求分配的棧容量超過java虛擬機棧所允許的最大容量,java虛擬機就會拋出StackOverfolwError

如果java虛擬機棧動態(tài)擴展,在擴展時沒有申請到足夠的內(nèi)存或者是創(chuàng)建新線程時沒有足夠的內(nèi)存再創(chuàng)建java虛擬機棧了,那么java虛擬機就會拋出outOfMemoryError

三、本地方法棧(Native Method Stack)

與虛擬機棧類似,區(qū)別是虛擬機棧執(zhí)行java方法,本地方法站執(zhí)行native方法。在虛擬機規(guī)范中對本地方法棧中方法使用的語言、使用方法與數(shù)據(jù)結(jié)構(gòu)沒有強制規(guī)定,因此虛擬機可以自由實現(xiàn)它。本地方法棧可以拋出StackOverflowError和OutOfMemoryError異常。不過這塊區(qū)域我們不怎么去關(guān)心。

四、Java堆

Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建,用來存放對象實例。是內(nèi)存中最大的一塊區(qū)域。垃圾收集器(GC)在該區(qū)域回收不使用的對象的內(nèi)存空間。但是并不是所有的對象都在這保存,深入理解java虛擬機中說道,隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)逐漸成熟,棧上分配、標量調(diào)換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化,所有的對象都分配在堆上也逐漸變得不那么絕對了。

堆的大小可以固定也可以動態(tài)擴展,可通過-Xms(最小值)和-Xmx(最大值)參數(shù)設(shè)置,如果在堆中沒有內(nèi)存完成實例分配,且堆也無法在擴展時,會拋出OutOfMemoryError異常。

下面給一張java 堆的結(jié)構(gòu)圖,

為了支持垃圾收集,堆被分為三個部分:

年輕代 : 常常又被劃分為Eden區(qū)和Survivor(From Survivor To Survivor)區(qū)(Eden空間、From Survivor空間、To Survivor空間(空間分配比例是8:1:1)

老年代:

永久代 :(jdk 8已移除永久代,取而代之的是元空間。下面會講解)

五、方法區(qū)

方法區(qū)也是所有線程共享。主要用于存儲類的信息、常量池、靜態(tài)變量、及時編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)邏輯上屬于堆的一部分。通常又叫“Non-Heap(非堆)”。

第二部分:使用例子理解java7內(nèi)存結(jié)構(gòu)

一個例子理解全部

為了理解的比較深刻,先給一個例子。通過例子講解印象更加深刻吧,假設(shè)我們在idea或者是任何IDE環(huán)境中定義了一個類。

有一個person類

public class Person{

int age;

String name;

Baby baby;

public void walk() {

System.out.println("我正在走路。。。。");

}

}

還有個Baby類

public class Baby{

String babyname;

int babyAge;

public void cry(){

System.out.println("我是孩子,我會哭");

}

}

最后是一個測試類Test

public class Test {

public static void main(String[] args) {

Person person = new Person();

person.name = "馮冬冬的IT技術(shù)棧";

person.age = 18;

person.walk();

?

Baby baby= new Baby();

baby.babyname = "馮XX";

System.out.println(baby.babyname);

person.baby = baby;

System.out.println(pserson.baby.cry);

}

}

好了有了上面的環(huán)境,接下來就開始分析這些代碼在運行時內(nèi)存的變化?,F(xiàn)在在我們的IDE開始運行。

第一步,JVM去方法區(qū)尋找Test類的代碼信息,如果有直接調(diào)用,沒有的話使用類的加載機制把類加載進來。同時把靜態(tài)變量、靜態(tài)方法、常量加載進來。這里加載的是(“馮冬冬的IT技術(shù)?!?,“馮XX”);這是因為字符串是常量,age中的18是基本類型。

第二步,jvm進入main方法,看到Person person=new Person()。首先分析Person這個類,同樣的尋找Person類的代碼信息,有就加載,沒有的話類加載機制加載進來。同時也加載靜態(tài)變量、靜態(tài)方法、常量(“我正在走路。。?!保?/p>

第三步,jvm接下來看到了person,person在main方法內(nèi)部,因而是局部變量,存放在棧空間中。

第四步,jvm接下來看到了new Person()。new出的對象(實例),存放在堆空間中。

第五步,jvm接下來看到了“=”,把new Person的地址告訴person變量,person通過四字節(jié)的地址(十六進制),引用該實例。 是不是有點暈,別著急,畫個圖看一下。

第六步,jvm看到person.name = "馮冬冬的IT技術(shù)棧";person通過引用new Person實例的name屬性,該name屬性通過地址指向常量池的"馮冬冬的IT技術(shù)棧"。

第七步,jvm看到person.age = 18; person的age屬性是基本數(shù)據(jù)類型,直接賦值。

第八步,jvm看到person.walk(); 調(diào)用實例的方法時,并不會在實例對象中生成一個新的方法,而是通過地址指向方法區(qū)中類信息的方法。走到這一步再看看圖怎么變化的。

第九步,jvm看到Baby baby=new Baby().這個過程和Person person = new Person()一樣

第十步,jvm看到baby.babyname = "馮XX";這個過程也和person.name = "馮冬冬的IT技術(shù)棧";一樣。

第十一步,jvm看到person.baby = baby;把baby對象引用賦值給Person實例的baby屬性屬性。

好了,到了這一步,應(yīng)該對java7的內(nèi)存結(jié)構(gòu)有一個詳細的認識了。

第三部分:java8內(nèi)存結(jié)構(gòu)

其實在第一部分的方法區(qū)介紹里面,已經(jīng)提前說了一些,想要好好的理解java8內(nèi)存結(jié)構(gòu),那一定是在java7的基礎(chǔ)上和其作比較,因此首先解釋一下兩個名詞:永久代(PermGen)和元空間(Metaspace)。

首先是永久代:

我們常見的 "java.lang.OutOfMemoryError: PermGen space "這個異常。這里的 “PermGen space”其實指的就是方法區(qū)。不過方法區(qū)和“PermGen space”又有著本質(zhì)的區(qū)別。前者是 JVM 的規(guī)范,而后者則是 JVM 規(guī)范的一種實現(xiàn),并且只有 HotSpot 才有 “PermGen space”。由于方法區(qū)主要存儲類的相關(guān)信息,所以對于動態(tài)生成類的情況比較容易出現(xiàn)永久代的內(nèi)存溢出。

然后是元空間

元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。因此,默認情況下,元空間的大小僅受本地內(nèi)存限制。

先給出java8的內(nèi)存結(jié)構(gòu)圖。

需要注意內(nèi)存模型與內(nèi)存結(jié)構(gòu)不同。

在內(nèi)存結(jié)構(gòu)中,其中Java堆和方法區(qū)的區(qū)域是多個線程共享的數(shù)據(jù)區(qū)域。也就是說,多個線程可能可以操作保存在堆或者方法區(qū)中的同一個數(shù)據(jù)。

在內(nèi)存模型中,其實JMM并不是是真實存在的。他只是一個抽象的概念。我們知道在多線程通信時候會存在一系列如可見性、原子性、順序性等問題,而JMM就是針對這些問題而建立的模型。

一、java7到j(luò)ava8的第一部分變化:元空間

下面來一張圖看一下java7到8的內(nèi)存模型吧(這個是在網(wǎng)上找的圖,如有侵權(quán)問題請聯(lián)系我刪除。)

二、java7到j(luò)ava8的第二部分變化:運行時常量池

運行時常量池(Runtime Constant Pool)的所處區(qū)域一直在不斷的變化,在java6時它是方法區(qū)的一部分;1.7又把他放到了堆內(nèi)存中;1.8之后出現(xiàn)了元空間,它又回到了方法區(qū)。

如果有問題,請批評指正

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容