在一開始學(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ū)。
如果有問題,請批評指正