天天看點

用最通俗的方法講JVM [一] ────JVM記憶體模型

目錄

        • 什麼是JVM?什麼是JVM記憶體模型?
          • 添加一個虛拟機
        • 1. 本地方法棧 (線程私有)
        • 2. 堆 (線程共有)
            • 2.1 存放内容
            • 2.2 生命周期
            • 2.3 另外說句題外話.
        • 3. 棧 (線程私有)
            • 3.1 存放内容
            • 3.2 生命周期
        • 4. 中繼資料區 (線程共有) 也叫元空間
            • 4.1 存放内容
            • 4.2 生命周期
        • 5. 寄存器 (線程私有)
        • 6. CodeCache (線程共享)
    • 參考文章

什麼是JVM?什麼是JVM記憶體模型?

備注:本文講的基于JDK1.8,且1.8之前和之後差距略大,本文對1.8之前的版本隻會略微介紹.

JVM說白了,就是個程式,而這個程式運作起來後,就是台計算機,而且和我們平時使用的計算機非常相似,他就是一台虛拟計算機.

那什麼是JVM記憶體模型?就是幾個大神寫了一個在計算機上運作的虛拟計算機的記憶體模型.

那計算機的記憶體模型是什麼樣的?

用最通俗的方法講JVM [一] ────JVM記憶體模型

各部分功能,相信不從事該行業的人都有相當一部分知道他的大概作用,但我們還是粗略解釋一下

名稱 速度 介紹
寄存器 速度特别快 暫存指令等短小精幹的資料.
速度塊 空間連續.
速度慢 空間不連續,但比硬碟可快多了.
硬碟 速度最慢 就是個倉庫.

那麼!本篇文章就會在此圖中進行講解

下面多圖慎入!

添加一個虛拟機

接下來,我們在這個電腦上添加一個虛拟機

既然我們說虛拟機和計算機是一樣的,那我們就把上述的堆棧等一堆東西都建一個放進電腦裡.

那麼放到哪呢?

  • 寄存器放不了.
  • 棧太小.
  • 堆可以,空間不連續我們可以自己搞.
  • 硬碟是一個實體存儲,也不行.

由上得出,放到堆裡,于是有了下面的樣子.

用最通俗的方法講JVM [一] ────JVM記憶體模型

這個圖也很好懂,就是把寄存器,堆,棧,硬碟都放到作業系統的堆中了.

OK,我們把虛拟機放進來了,那麼接下來呢?好像沒什麼頭緒.

既然虛拟機有了,那我們把它運作起來吧.

現在有兩個問題

  1. 它是怎麼運作的?

    JVM就是個C語言程式

  2. 這個程式的功能是什麼?

    運作的是.class檔案.

簡單的說,這個程式在運作的時候,會啟動一個功能,叫類加載器,這個類加載器加載.class檔案後,會把檔案中的不同内容,放入到堆棧這些不同的區域中.

那麼這些區域都分别放了寫什麼呢?

區域名稱 存儲内容 特點
寄存器 代碼運作到了哪一行(行話:目前線程正在執行的位元組碼的行号訓示器) 空間小,不會溢出,随線程生滅
本地方法棧 JVM執行的native方法 HotSpot虛拟機不區分虛拟機棧和本地方法棧,兩者是一塊的
1.局部變量 2.操作棧 3,動态連結 4.傳回位址 先進後出,桶式結構
1.執行個體對象 2.數組 3.字元串常量池 4.靜态常量 垃圾回收器會回收沒被引用的對象和數組
中繼資料區(1.8前叫方法區) 1.類資訊 2.編譯後的代碼 3.運作時常量池 1.7前叫方法區,在堆中稱為非堆,1.7後放入了本地記憶體,叫中繼資料區

接下來我一個個詳細解釋一下

1. 本地方法棧 (線程私有)

這個知識點比較簡單,本地方法棧服務的對象是JVM執行的native方法

總之,線程開始調用本地方法時,不受JVM限制.太多的nativa方法會影響虛拟機的可移植性.

2. 堆 (線程共有)

為什麼把堆放在棧前講,是因為這部分比較重要,而且是基礎部分.

堆中的内容是線程共有的,所有線程通路堆是同一個區域.

2.1 存放内容

堆中存放的資料是對象執行個體和數組

例如:

User user = new User();//User是系統中常見的Model類
             ↑
             └─ new 出來的這個東西,就在堆中,controller同理
                     ↓
UserController uc = new UserController();//mvc模式下常見的類
           

堆最大,裡面的東西也最多.裡面的東西越放越多,但記憶體就那麼大,總有放滿的一天,于是,堆中沒用的東西就要被回收.

于是這群大神将堆分了幾個區,分别為:

字元串常量池 : 其實是C++寫的一個hash表,所有的字元串都儲存在常量池中.
			 在http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/9732f3600a48/src/share/vm/classfile/symbolTable.hpp定義
老年代 : 比例約為 2
新生代 : 比例約為 1
	其中新生代又分為:
	Eden區        : 占新生代的 8/10
	Suivivor 0 區 : 占新生代的 1/10
	Suivivor 1 區 : 占新生代的 1/10
//當然大小和比例可以通過指令來修改
           

如圖:

用最通俗的方法講JVM [一] ────JVM記憶體模型

再換一張官方的圖

用最通俗的方法講JVM [一] ────JVM記憶體模型
這張圖可以使用JDK自帶的 : jdk/bin/jvisualvm.exe.
打開後選擇 - 工具 - 插件 - 可用插件 - 安裝VisualGC - 重新開機軟體 - 左側選擇JVM程序 - 右邊就會顯示Visual GC
           
用最通俗的方法講JVM [一] ────JVM記憶體模型

那麼JVM對各個區域是如何使用的呢?

  1. 絕大部分對象生成時都在Eden區,當Eden區裝填滿的時候,會觸發Young GC。
  2. Young GC的時候,在Eden區執行清除,沒有被引用的對象直接回收,依然存活的對象會被移送到Survivor區.

Survivor 區分為S0和S1兩塊記憶體空間,送到哪塊空間呢?

  1. 每次Young GC的時候,将存活的對象複制到未使用的那塊空間,然後将目前正在使用的空間完全清除,交換兩塊空間的使用狀态.
  2. 如果Young GC要移送的對象大于Survivor區容量上限,則直接移交給老年代.

那會不會有頑強對象一直留在Surivivor區呢?

答案是不會的,每個對象都有一個計數器,每次YGC都會加1.計數器預設為15,如果某個對象在Survivor 區交換14次之後,則晉升至老年代.

2.2 生命周期

對象在堆的生命周期如下:

用最通俗的方法講JVM [一] ────JVM記憶體模型

至于虛拟機如何将對象标記為未被引用,可以檢視 : GC算法.

2.3 另外說句題外話.

為什麼計算機學科中将這塊區域叫為堆(heap),而不是其他任何名詞呢?

其實是因為這裡的資料是不連續的,也就是配置設定記憶體位址是這裡一個,那裡一個.

如圖:

用最通俗的方法講JVM [一] ────JVM記憶體模型

堆的記憶體是不整齊的,是亂的.是非連續的,就是一堆雜亂的東西,是以稱之為堆.

3. 棧 (線程私有)

棧中存放的是什麼?

棧中其實就是和目前執行方法相關的資料.

棧首先有個首要的特點,他是桶狀的,是一個先入後出(FILO)的資料結構.如圖:

用最通俗的方法講JVM [一] ────JVM記憶體模型

但棧是線程私有的,而我們的系統通常不隻有一個線程,是以棧實際中應當是這樣的,如圖:

用最通俗的方法講JVM [一] ────JVM記憶體模型

3.1 存放内容

那圖中這些都是什麼呢?我們來結合圖來說:

  1. 空棧   : 首先棧中原本是空的
    用最通俗的方法講JVM [一] ────JVM記憶體模型
  2. 建立棧  : 在某個線程建立時,虛拟機會為線程建立一個該線程私有的棧.
    用最通俗的方法講JVM [一] ────JVM記憶體模型
  3. 建立棧幀 : 線程開始執行到第一個方法時,就會在棧中建立一個棧幀,而最新建立的棧幀稱為目前棧幀
    用最通俗的方法講JVM [一] ────JVM記憶體模型

棧幀中存儲的是該方法的一系列資訊,包括如下:

1. 局部變量表
	用于存放方法參數和方法内部定義的局部變量
	局部變量表的容量以變量槽 [Slot] 為最小機關。
	在編譯期由Code屬性中的 [max_locals] 确定局部變量表的大小.
	
2. 操作數棧
	可以了解成在哪裡執行目前的這一行代碼.
	
3. 動态連結
	在運作時将類常量池中的符号引用轉換為直接引用.
	簡單來說,就是我們的類在編譯好後,并不知道其中的代碼所調用的方法的位址是什麼.
	隻有在執行到該方法時,才知道調用的具體是哪個執行個體的方法.

4. 方法傳回位址
	其實就是标記一個退出的指令,或是遇到異常.則傳回到上層棧幀.
	
	下面是術語,可以加深了解
	當一個方法開始執行後,隻有兩種方式可以退出,一種是遇到方法傳回的位元組碼指令;一種是遇見異常,并且這個異常沒有在方法體内得到處理。
	無論采用何種退出方式,在方法退出之後,都需要傳回到方法被調用的位置,程式才能繼續執行,方法傳回時可能需要在棧幀中儲存一些資訊,用來	幫助恢複它的上層方法的執行狀态。一般來說,方法正常退出時,調用者的PC計數器的值可以作為傳回位址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,傳回位址是要通過異常處理器表來确定的,棧幀中一般不會儲存這部分資訊。
	方法退出的過程實際上就等同于把目前棧幀出棧,是以退出時可能執行的操作有:恢複上層方法的局部變量表和操作數棧,把傳回值(如果有的話)壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。
           

Java在執行時,就是将各種指令往棧中寫入和提取

3.2 生命周期

檢視一段代碼的位元組碼可以更好的了解JVM是如何對操作數棧和局部變量表進行操作的.

package com.jasmine.Java進階.JVM.位元組碼;

public class TestJVMStack {
    public static int a = 123;
    public int simpleMethod(){
        int x = 13;
        int y = 14;
        int z = x + y;

        return z;
    }

    public static void main(String[] args) {
        TestJVMStack s = new TestJVMStack();
        System.out.println(s.simpleMethod());
    }
}
           

上述代碼的位元組碼為:(略長,不想了解可直接到下面看對于操作棧的操作.)

Classfile /E:/WorkSpace/Idea/MyJava/target/classes/com/jasmine/Java進階/JVM/位元組碼/TestJVMStack.class
  Last modified 2019-8-27; size 854 bytes
  MD5 checksum 15fab830f998782e5087b8626274d45c
  Compiled from "TestJVMStack.java"
public class com.jasmine.Java進階.JVM.位元組碼.TestJVMStack
  minor version: 0
  major version: 52
  /*
  類的通路辨別
  ACC_PUBLIC:代表public
  ACC_SUPER :用于相容早期的編譯器,新編譯器都設定該标記.
  */
  flags: ACC_PUBLIC, ACC_SUPER
  // 類常量池,也叫 Class常量池
  // 第一列為常量類型
  // 第二清單示引用的常量或者utf8類型常量值
  // 如#1的類型是class,引用的是#2的值
Constant pool:
   #1 = Class              #2             // com/jasmine/Java進階/JVM/位元組碼/TestJVMStack
   #2 = Utf8               com/jasmine/Java進階/JVM/位元組碼/TestJVMStack
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <clinit>       //代表是類初始化階段
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Fieldref           #1.#11         // com/jasmine/Java進階/JVM/位元組碼/TestJVMStack.a:I
  #11 = NameAndType        #5:#6          // a:I
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               <init>         // 代表是執行個體初始化階段,說白了就是構造方法
  #15 = Methodref          #3.#16         // java/lang/Object."<init>":()V
  #16 = NameAndType        #14:#8         // "<init>":()V
  #17 = Utf8               this
  #18 = Utf8               Lcom/jasmine/Java進階/JVM/位元組碼/TestJVMStack;
  #19 = Utf8               simpleMethod
  #20 = Utf8               ()I
  #21 = Utf8               x
  #22 = Utf8               y
  #23 = Utf8               z
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Methodref          #1.#16         // com/jasmine/Java進階/JVM/位元組碼/TestJVMStack."<init>":()V
  #27 = Fieldref           #28.#30        // java/lang/System.out:Ljava/io/PrintStream;
  #28 = Class              #29            // java/lang/System
  #29 = Utf8               java/lang/System
  #30 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Methodref          #1.#34         // com/jasmine/Java進階/JVM/位元組碼/TestJVMStack.simpleMethod:()I
  #34 = NameAndType        #19:#20        // simpleMethod:()I
  #35 = Methodref          #36.#38        // java/io/PrintStream.println:(I)V
  #36 = Class              #37            // java/io/PrintStream
  #37 = Utf8               java/io/PrintStream
  #38 = NameAndType        #39:#40        // println:(I)V
  #39 = Utf8               println
  #40 = Utf8               (I)V
  #41 = Utf8               args
  #42 = Utf8               [Ljava/lang/String;
  #43 = Utf8               s
  #44 = Utf8               SourceFile
  #45 = Utf8               TestJVMStack.java
{
  // 代表有一個靜态變量a,修飾是public static
  public static int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      // stack     : 最大操作數棧,JVM運作時會根據這個值來配置設定棧幀(Frame)中的操作棧深度,此處為1
      // locals    : 局部變量所需的存儲空間,機關為Slot,Slot是虛拟機為局部變量配置設定記憶體時所使用的最小機關,為4個位元組大小.
      // args_size : 方法參數的個數,這裡是0
      stack=1, locals=0, args_size=0
         0: bipush        123
         2: putstatic     #10                 // Field a:I
         5: return
      // LineNumberTable 該屬性的作用是描述源碼行号與位元組碼行号(位元組碼偏移量)之間的對應關系。
      LineNumberTable:
        line 60: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public com.jasmine.Java進階.JVM.位元組碼.TestJVMStack();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #15                 // Method java/lang/Object."<init>":()V
         4: return
      // LineNumberTable 該屬性的作用是描述源碼行号與位元組碼行号(位元組碼偏移量)之間的對應關系。
      LineNumberTable:
        line 6: 0
      // LocalVariableTable 該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關系。
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jasmine/Java進階/JVM/位元組碼/TestJVMStack;

  public int simpleMethod();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      // 這裡普通的方法參數的個數為1是因為所有類中的方法都有個隐藏參數this
      stack=2, locals=4, args_size=1
         /*******************************************************
          * 對操作數棧的操作主要看這裡,下面有對這段的較長的描述
          ******************************************************/
         0: bipush        13  
         2: istore_1          
         3: bipush        14  
         5: istore_2          
         6: iload_1           
         7: iload_2           
         8: iadd              
         9: istore_3          
        10: iload_3           
        11: ireturn           
      LineNumberTable:
        line 62: 0
        line 63: 3
        line 64: 6
        line 66: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lcom/jasmine/Java進階/JVM/位元組碼/TestJVMStack;
            3       9     1     x   I
            6       6     2     y   I
           10       2     3     z   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #1                  // class com/jasmine/Java進階/JVM/位元組碼/TestJVMStack
         3: dup
         4: invokespecial #26                 // Method "<init>":()V
         7: astore_1
         8: getstatic     #27                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: invokevirtual #33                 // Method simpleMethod:()I
        15: invokevirtual #35                 // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 70: 0
        line 71: 8
        line 72: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  args   [Ljava/lang/String;
            8      11     1     s   Lcom/jasmine/Java進階/JVM/位元組碼/TestJVMStack;
}
SourceFile: "TestJVMStack.java"
           

上述位元組碼中的下段代碼就是JVM對操作棧的執行順序.

// 對應代碼 13;
         0: bipush        13  // 将一個8位帶符号整數 13 壓入操作棧頂
         // 對應代碼 x = 13;
         2: istore_1          // 從棧頂彈出,并将int類型值存入局部變量表的slot_1中
         // 對應代碼 14;
         3: bipush        14  // 将一個8位帶符号整數 14 壓入操作棧頂
         // 對應代碼 y = 14;
         5: istore_2          // 從棧頂彈出,并将int類型值存入局部變量表的slot_2中
         // 對應代碼 x;
         6: iload_1           // 從局部變量表的slot_1中裝載int類型值,壓入操作棧頂
         // 對應代碼 y;
         7: iload_2           // 從局部變量表的slot_2中裝載int類型值,壓入操作棧頂
         // 對應代碼 x + y;
         8: iadd              // 操作數棧中的前兩個int相加,并将結果壓入操作數棧頂
         // 對應代碼 z = x + y;
         9: istore_3          // 從棧頂彈出,并将int類型值存入局部變量表的slot_3中
         // 對應代碼 z;
        10: iload_3           // 從局部變量表的slot_3中裝載int類型值,壓入操作棧頂
         // 對應代碼 return z;
        11: ireturn           // 傳回棧頂元素
           

由上可見,每次操作其實都是對棧頂或棧頂的多個連續的操作棧進行操作.方法執行完後,會根據方法傳回位址,傳回上層方法,也就是上一個棧幀,如果全部棧幀都執行完,就認為該線程的内容執行完畢,線程結束生命周期.

4. 中繼資料區 (線程共有) 也叫元空間

JDK 1.7 之前

Java虛拟機規範中定義方法區是堆的一個邏輯部分,但是别名Non-Heap(非堆),以與Java堆區分.

JDK 1.8

将方法區從堆中移了出來,放入了本地記憶體中,并且改名為中繼資料區,這是不同版本虛拟機變化最大的地方.

中繼資料區和堆一樣,都是線程共享的.整個虛拟機中隻有一個中繼資料區.

中繼資料區的大小受到本機記憶體容量限制,并且允許指定大小,若不指定,中繼資料區會根據應用程式運作時的需求動态設定大小

中繼資料區的大小如果達到參數[MaxMetaspaceSize]設定的值,将會觸發對死亡對象和類加載器的回收.

4.1 存放内容

中繼資料區中存放已經被虛拟機加載的 :

1. 運作時常量池
   是Class常量池的運作時表現形式.
2. 字段和方法資料
3. 構造函數和普通方法的位元組碼内容

字面量和靜态變量被移到了堆中
           

如下圖:

用最通俗的方法講JVM [一] ────JVM記憶體模型

4.2 生命周期

中繼資料區其實是由一個個的類加載器存儲區組成的.當類加載器不再存活,則該類加載器對應的中繼資料區被回收.

5. 寄存器 (線程私有)

每一個線程都包含自己的寄存器,儲存目前線程執行到了哪一行.

6. CodeCache (線程共享)

還有一部分,順帶一提

CodeCache是代碼緩存區

主要存放JIT所編譯的代碼

還有Java所使用的本地方法代碼也會存儲在codecache中.

不同的jvm、不同的啟動方式codecache的預設值大小也不盡相同。

他也獨立在堆之外,是線程共享的

JIT : 在部分商用虛拟機中(如HotSpot),Java程式最初是通過解釋器(Interpreter)進行解釋執行的,當虛拟機發現某個方法或代碼塊的運作特别頻繁時,就會把這些代碼認定為“熱點代碼”。為了提高熱點代碼的執行效率,在運作時,虛拟機将會把這些代碼編譯成與本地平台相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器.

到此

我們介紹了6個子產品,分别為:

1. PC寄存器(程式計數器)
2. 本地方法棧
3. 虛拟機棧
4. 堆
5. 元空間
6. CodeCache
           

那麼,最開始那張圖就變成了這樣:

用最通俗的方法講JVM [一] ────JVM記憶體模型

這就是Java的記憶體模型了

上面說到的3個常量池

  1. 字元串常量池
  2. 運作時常量池
  3. 類常量池

我們放到以後再講

參考文章

https://blog.csdn.net/xyh930929/article/details/84067186

https://blog.csdn.net/championhengyi/article/details/78760590

JAVA8中的元空間到底存了什麼? : https://www.jianshu.com/p/474d98fc4776

什麼是JIT : https://www.cnblogs.com/dzhou/p/9549839.html