jvm系列(1)內存結構(補充版)

在一開始學習java的時候,那時候是在網上看視頻,老師就常常提到什麼對象分配在堆區,什麼在棧區,那時候和理解,後來理解了就想着寫一篇文章好好的去梳理一下。
java

想說一下這篇文章的脈絡:數據結構

首先,研究java7的內存結構,並對其進行一個詳細的介紹,由於理解了java7以後java8比較容易理解多線程

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

最後,咱們給出java8的內存結構,看一看作了哪些改動,並和java7進行一個比較。jvm

第一部分:java7內存結構

先給一張java7的內存結構圖吧(我用Windows裏面的畫圖工具畫的,因此看起來不怎麼美觀)ide

圖片

首先對這個圖有一個認識,從上面能夠看到java7的內存結構大體分了五個部分:PC寄存器,java虛擬機棧、本地方法棧、java堆、方法區。其中PC寄存器、java虛擬機棧和本地方法棧是全部線程共享的一塊內存區域。java堆和方法區是每個線程隔離的一塊區域,其中,方法區還有一個運行時常量池。
工具

接下來看一看每一塊區域裏面存放的什麼?學習

1、PC寄存器測試

在大學的時候學過計算機組成原理的時候都知道,內存裏面有不少寄存器,大概幾百個吧(目前的,以前大學學的時候老師說才幾十個),每一種寄存器的用途都不同,其中有一個寄存器就是程序計數器。這個寄存器的主要做用就是存放下一條須要執行的指令。優化

首先,爲何要有這個程序計數器呢?這是由於咱們的處理器在一個時刻,只能執行一個線程中的指令。可是咱們的程序每每都是多線程的,這時候處理器就須要來回切換咱們的線程,爲了在線程切換以後回到以前正確的位置上,此時就須要一個程序計數器,這也就很容易理解了咱們的每一個線程都有一個本身的程序計數器來保存本身以前的狀態。

接下來如何理解這個程序計數器的功能呢?假如咱們的程序代碼假如是一行一行執行的,程序計數器永遠指向下一行須要執行的字節碼指令。在循環結構中,咱們就能夠改變程序計數器中的值,來指向下一條須要執行的指令。所以,在分支、循環、跳轉、異常處理和線程恢復等等一些場景都須要這個程序計數器來完成。

最後看一下在什麼狀況下,應該存儲什麼內容。《java虛擬機規範》中說若是當前執行的是 Java 的方法,則該寄存器中保存當前執行指令的地址;假若執行的是native 方法,則PC寄存器中爲空(Undefined)。PC寄存區區域就是存放了N多個這樣的寄存區。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。所以能夠把他的幾個特色概括以下。

  1. 程序計數器指定下一條須要執行的指令

  2. 每個線程獨有一個程序計數器

  3. 執行java代碼時,寄存器保存當前指令地址

  4. 執行native方法時候,寄存器爲空。

  5. 不會形成OutOfMemoryError狀況

2、Java虛擬機棧

每個線程都有本身的java虛擬機棧,這個棧與線程同時建立,一個線程中的每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。每一個線程有一個私有的棧,隨着線程的建立而建立。棧裏面存着的是一種叫「棧幀」的東西,每一個方法會建立一個棧幀,棧幀中存放了局部變量表(基本數據類型和對象引用)、操做數棧、動態鏈接和返回地址等信息。當前運行方法對應的棧幀叫作當前棧幀。下面主要對這個棧幀進行一個介紹。

先看一張圖

圖片

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

  • 64位長度的long和double類型佔用2個局部變量空間(Slot),其他數據類型只佔用一個。

  • 局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,

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

       接下來操做數棧,其實在棧幀剛剛建立的時候,操做數棧是空的,java虛擬機能夠從局部變量表或者對象的實例字段中,複製一些常量或者變量值到操做數棧中。也能夠從操做數棧中取走數據。他的深度在編譯期就已經肯定了。

     動態鏈接是什麼意思呢?在這裏咱們先有個基本的印象,下面舉例子的時候,再來看這個解釋比較容易理解一點,咱們知道,在線程中一個方法去調用另一個方法,是經過符號引用來實現的,動態鏈接的做用就是把這個符號引用表示的方法轉化爲實際方法的直接引用。

對於java虛擬機棧的描述,最後看一下可能發生的異常狀況:

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

  • 若是java虛擬機棧動態擴展,在擴展時沒有申請到足夠的內存或者是建立新線程時沒有足夠的內存再建立java虛擬機棧了,那麼java虛擬機就會拋出outOfMemoryError

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

與虛擬機棧相似,區別是虛擬機棧執行java方法,本地方法站執行native方法。在虛擬機規範中對本地方法棧中方法使用的語言、使用方法與數據結構沒有強制規定,所以虛擬機能夠自由實現它。本地方法棧能夠拋出StackOverflowError和OutOfMemoryError異常。不過這塊區域咱們不怎麼去關心。

4、Java堆

Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立,用來存放對象實例。是內存中最大的一塊區域。垃圾收集器(GC)在該區域回收不使用的對象的內存空間。可是並非全部的對象都在這保存,深刻理解java虛擬機中說道,隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量調換優化技術將會致使一些微妙的變化,全部的對象都分配在堆上也逐漸變得不那麼絕對了。

堆的大小能夠固定也能夠動態擴展,可經過-Xms(最小值)和-Xmx(最大值)參數設置,若是在堆中沒有內存完成實例分配,且堆也沒法在擴展時,會拋出OutOfMemoryError異常。

下面給一張java 堆的結構圖,

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

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

老年代:

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

5、方法區

方法區也是全部線程共享。主要用於存儲類的信息、常量池、靜態變量、及時編譯器編譯後的代碼等數據。方法區邏輯上屬於堆的一部分。一般又叫「Non-Heap(非堆)」。

第二部分:使用例子理解java7內存結構

一個例子理解所有

爲了理解的比較深入,先給一個例子。經過例子講解印象更加深入吧,假設咱們在idea或者是任何IDE環境中定義了一個類。

有一個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技術棧";
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);
}
}

好了有了上面的環境,接下來就開始分析這些代碼在運行時內存的變化。如今在咱們的IDE開始運行。

  1. 第一步,JVM去方法區尋找Test類的代碼信息,若是有直接調用,沒有的話使用類的加載機制把類加載進來。同時把靜態變量、靜態方法、常量加載進來。這裏加載的是(「馮鼕鼕的IT技術棧」,「馮XX」);這是由於字符串是常量,age中的18是基本類型。

  2. 第二步,jvm進入main方法,看到Person person=new Person()。首先分析Person這個類,一樣的尋找Person類的代碼信息,有就加載,沒有的話類加載機制加載進來。同時也加載靜態變量、靜態方法、常量(「我正在走路。。。」)

  3. 第三步,jvm接下來看到了person,person在main方法內部,於是是局部變量,存放在棧空間中。

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

  5. 第五步,jvm接下來看到了「=」,把new Person的地址告訴person變量,person經過四字節的地址(十六進制),引用該實例。 是否是有點暈,彆着急,畫個圖看一下。

    圖片

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

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

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

    圖片

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

  10. 第十步,jvm看到baby.babyname = "馮XX";這個過程也和person.name = "馮鼕鼕的IT技術棧";同樣。

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

好了,到了這一步,應該對java7的內存結構有一個詳細的認識了。

第三部分:java8內存結構

其實在第一部分的方法區介紹裏面,已經提早說了一些,想要好好的理解java8內存結構,那必定是在java7的基礎上和其做比較,所以首先解釋一下兩個名詞:永久代(PermGen)和元空間(Metaspace)。

首先是永久代:

咱們常見的 "java.lang.OutOfMemoryError: PermGen space "這個異常。這裏的 「PermGen space」其實指的就是方法區。不過方法區和「PermGen space」又有着本質的區別。前者是 JVM 的規範,然後者則是 JVM 規範的一種實現,而且只有 HotSpot 纔有 「PermGen space」。因爲方法區主要存儲類的相關信息,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。

而後是元空間

元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。

先給出java8的內存結構圖。

圖片

須要注意內存模型與內存結構不一樣

在內存結構中,其中Java堆和方法區的區域是多個線程共享的數據區域。也就是說,多個線程可能能夠操做保存在堆或者方法區中的同一個數據。

在內存模型中,其實JMM並非是真實存在的。他只是一個抽象的概念。咱們知道在多線程通訊時候會存在一系列如可見性、原子性、順序性等問題,而JMM就是針對這些問題而創建的模型。

圖片

1、java7到java8的第一部分變化:元空間

下面來一張圖看一下java7到8的內存模型吧(這個是在網上找的圖,若有侵權問題請聯繫我刪除。)

圖片


2、java7到java8的第二部分變化:運行時常量池

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

相關文章
相關標籤/搜索