JVM內存模型有這篇文章就夠了

一、你瞭解JVM內存模型嗎

在這之前需要知道

內存尋址過程
在這裏插入圖片描述
地址空間劃分

  • 內核空間是用於連接硬件,調度程序聯網等服務
  • 用戶空間,纔是java運行的系統空間

我們知道JVM是內存中的虛擬機,主要使用內存進行存儲,所有類、類型、方法,都是在內存中,這決定着我們的程序運行是否健壯、高效。

JVM內存模型圖——JDK1.8

在這裏插入圖片描述

  • 線程私有:程序計數器、虛擬機棧、本地方法棧
  • 線程共享:MetaSpace、Java堆
    下面我們會對圖中五個部分進行詳細說明

1.1、程序計數器

  • 當前線程所執行的字節碼行號指示器(邏輯)
  • 通過改變計數器的值來選取下一條需要執行的字節碼指令
  • JVM的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器只會執行一條線程中的指令,爲了線程切換後能夠恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程計數器不會互相影響。所以,程序計數器和線程是一對一的關係即(線程私有
  • 對Java方法計數,如果是Native方法則計數器值爲Undefined,Native方法是由非Java代碼實現的外部接口
  • 程序計數器是爲了防止內存泄漏
    在後邊的舉例中我們可以看到程序計數器的作用。

1.2、Java虛擬機棧(Stack)

  • Java方法執行的內存模型
  • 生命週期和線程是相同的,每個線程都會有一個虛擬機棧,棧的大小在編譯期就已經確定了
  • 棧的變量隨着變量作用域的結束而釋放,不需要jvm垃圾回收機制回收。
  • 包含多個棧幀
    • 棧幀包含
      • 局部變量表
        • 包含方法執行過程中的所有變量(所有類型)
      • 操作數棧
        • 入棧、出棧、複製、交換、產生消費變量
      • 動態連接
      • 返回地址
        +在這裏插入圖片描述

在Java虛擬機棧中,一個棧幀對應一個方法,,方法執行時會在虛擬機棧中創建一個棧幀,而且當前虛擬機棧只能有一個活躍的棧幀,並且處於棧頂,當前方法結束後,可能會將返回值返回給調用它的方法,而自己將會被彈出棧(即銷燬),下一個棧頂將會被執行。

舉例說明:

ByteCodeSample.java

package com.mtli.jvm.model;

/**
 * @Description:測試JVM內存模型
 * @Author: Mt.Li
 * @Create: 2020-04-26 17:47
 */
public class ByteCodeSample {
    public static int add(int a , int b) {
        int c= 0;
        c = a + b;
        return c;
    }
}

對其進行編譯生成.class文件

javac com/mtli/jvm/model/ByteCodeSample.java

然後用javap -verbose 進行反編譯

javap -verbose com/mtli/jvm/model/ByteCodeSample.class

生成如下:

Classfile /E:/JavaTest/javabasic/java_basic/src/com/mtli/jvm/model/
ByteCodeSample.class
  Last modified 2020-4-26; size 289 bytes
  MD5 checksum 2421660bb241239f1a67171bb771521f
  Compiled from "ByteCodeSample.java"
public class com.mtli.jvm.model.ByteCodeSample
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// 描述類信息
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<ini
t>":()V
   #2 = Class              #13            // com/mtli/jvm/model/Byt
eCodeSample
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               add
   #9 = Utf8               (II)I
  #10 = Utf8               SourceFile
  #11 = Utf8               ByteCodeSample.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/mtli/jvm/model/ByteCodeSample
  #14 = Utf8               java/lang/Object
 // 以上是常量池(線程共享)
{
  public com.mtli.jvm.model.ByteCodeSample();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/O
bject."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
// 以上是初始化過程
  public static int add(int, int);
    descriptor: (II)I  // 接收兩個int類型變量
    flags: ACC_PUBLIC, ACC_STATIC // 描述方法權限和類型
    Code:
      stack=2, locals=3, args_size=2 // 操作數棧深度 、 容量  、參數數量
         0: iconst_0
         1: istore_2
         2: iload_0
         3: iload_1
         4: iadd
         5: istore_2
         6: iload_2
         7: ireturn
      LineNumberTable:
        line 10: 0 // 這裏的第0行對應我們代碼中的第10行
        line 12: 2
        line 13: 6
}
SourceFile: "ByteCodeSample.java"

執行add(1,2)

以下是程序在JVM虛擬機棧中的執行過程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WTPww4yl-1588037601095)(七、你瞭解Java的內存模型嗎.assets/image-20200426182038122.png)]
圖不是很清楚,我來說一下過程,最下邊的是程序計數器(前邊提到的),最上邊是操作指令,中間是局部變量表和操作數棧(位置從0開始)

  • 最開始,我們int c = 0,所以操作數棧頂初始值爲0,局部變量表存儲變量值。
  • istore_2 就是出棧的意思,將0放入變量表2的位置
  • iload_0 就是入棧,將1複製並壓入操作數棧
  • 然後將位置在1的值“2”壓入棧
  • 在棧中執行add方法,得到“3”
  • 將棧頂“3”取出到變量表的2位置
  • 再次將“3”壓入棧,準備return
  • 方法返回值

執行完之後,當前線程虛擬機棧的棧幀會彈出,對應的其他方法與當前棧幀的連接釋放、引用釋放,它的下一個棧幀成爲棧頂。

1.1.1、java.lang.StackOverflowError問題

我們知道,一個棧幀對應一個方法,存放棧幀的線程虛擬棧是有深度限制的,我們調用遞歸方法,每遞歸一次,就會創建一個新的棧幀壓入虛擬棧,當超出限度後,就會報此錯誤。

舉例說明:

package com.mtli.jvm.model;

/**
 * @Description:斐波那契
 * F(0)=0,F(1)=1,當n>=2的時候,F(n) = F(n-1) + F(n-2),
 * F(2) = F(1) + F(0) = 1,F(3) = F(2) + F(1) = 1+1 = 2
 * 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
 * @Author: Mt.Li
 * @Create: 2020-04-26 18:33
 */
public class Fibonacci {
    public static int fibonacci(int n) {
        if(n>=0){
            if(n == 0) {return 0;}
            if(n == 1) {return 1;}
            return fibonacci(n-1) +fibonacci(n-2);
        }
        return n;

    }

    public static void main(String[] args) {
        System.out.println(fibonacci(0));
        System.out.println(fibonacci(1));
        System.out.println(fibonacci(2));
        System.out.println(fibonacci(3));
        System.out.println(fibonacci(1000000));
        // java.lang.StackOverflowError
    }
}

結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kXQkjtgK-1588037891688)(七、你瞭解Java的內存模型嗎.assets/image-20200426185224796.png)]

解決方法是限制遞歸次數,或者直接用循環解決。

還有就是,由JVM管理的虛擬機棧數量也是有限的,也就是線程數量也是有限定。

由於棧幀在方法返回後會自動釋放,所有棧是不需要GC來回收的。

1.3、本地方法棧

  • 與虛擬機棧相似,主要作用於標註了native的方法

1.4、元空間(MetaSpace)

元空間(MetaSpace)在jdk1.7之前是屬於永久代(PermGen)的,兩者的作用就是記錄class的信息,jdk1.7中,永久代被移入堆中解決了前面版本的永久代分配內存不足時報出的OutOfMemoryError,jdk1.8之後元空間替代了永久代

  • 元空間使用本地內存,而永久代使用的是jvm的空間

1.4.1、MetaSpace相比PermGen的優勢

  • 字符串常量池存在永久代中,容易出現性能問題和內存溢出(空間大小不如元空間)
  • 類和方法的信息大小難以確定,給永久代的大小指定帶來了困難
  • 永久代會爲GC帶來不必要的複雜性
  • 方便HotSpot與其他JVM如Jrockit的集成

1.5、Java堆(Heap)

  • 對象實例的分配區域,實例在此處分配內存
  • java堆可以處於不連續的物理空間中,只要邏輯上是連續的即可
  • 是GC管理的主要區域,按照GC分代回收的方法,java堆又分爲新生代老生代(以後會出一篇GC相關的)

二、JVM三大性能調優參數 -Xms -Xmx -Xss的含義

  • -Xss:規定了每個線程虛擬機棧(堆棧)的大小(一般情況下256k足夠)
  • -Xms:堆的初始值
  • -Xmx:堆能達到的最大值

三、Java內存模型中堆和棧的區別——內存分配策略

需要先了解

  • 靜態存儲:編譯時確定每個數據目標在運行時的存儲空間需求,不允許有可變的程序存在,比如循環
  • 棧式存儲:數據區需求在編譯時未知,運行時模塊入口前確定。存儲局部變量,定義在方法中的都是局部變量,所以,方法先進棧,創建棧幀等操作,方法一旦返回,即變量離開作用域,則棧幀釋放,變量也會釋放。(生命週期短)
  • 堆式存儲:編譯時或運行時模塊入口都無法確定,動態分配。堆存儲的是數組和對象,存儲結構複雜,所需空間更多,哪怕是實體中的一個屬性數據消失,這個實體也不會消失。(生命週期長)

區別

  • 管理方式:棧自動釋放,堆需要GC
  • 空間大小:棧比堆小
  • 碎片相關:棧產生的碎片遠小於堆
  • 分配方式:棧支持靜態和動態分配,而堆僅支持動態分配
  • 效率:棧的效率比堆高,堆更靈活
  • 聯繫:引用對象、數組時,棧裏面定義變量保存堆中目標的首地址
    在這裏插入圖片描述

四、元空間、堆、線程獨佔部分間的聯繫——內存角度

我們來看下面這個例子:
在這裏插入圖片描述
以下是各個部分包含的內容:
在這裏插入圖片描述

  • 元空間裏面存着類的信息,比如方法、變量
  • java堆中存放對象實例
  • 線程獨佔:用來保存變量的值即變量的引用、對象的地址引用,記錄行號,用來記錄代碼的執行

五、不同JDK版本之間的intern()方法的區別——JDK6 VS JDK6+

說到這裏我們不得不提一下String.intern()方法在jdk版本變更中的不同

String s = new String("a");
s.intern();

JDK6:當調用intern方法時,如果字符串常量池先前已創建出該字符串對象,則返回池中的該字符串的引用。否則,將此字符串對象添加到字符串常量池中,並且返回該字符串對象的引用。

JDK6+:當調用intern方法時,如果字符串常量池先前已創建出該字符串對象,則返回池中的該字符串的引用。否則,如果該字符串對象已經存在於Java堆中,則將堆中,則將堆中對此對象的引用添加到字符串常量池中,並且返回該引用;如果堆中不存在,則在池中創建該字符串並返回其引用

我們看一個例子:

public class InternDifference {
    public static void main(String[] args) {
        String s = new String("a");
        s.intern();
        String s2 = "a";
        System.out.println(s == s2);

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 == s4);
    }
}

jdk1.8下運行結果爲

false
true

分析:

  • s在創建的時候使用new方式創建,這裏會在堆中就會有一個值爲"a"的對象,intern()之後,intern()會將首次遇到的字符串放到常量池中,此時常量池中就有"a",發現常量池中有"a"。創建s2的時候,看到常量池中已經有"a"了,於是,s2直接指向常量池"a"的地址,而s是指向堆中對象的地址,故返回false
  • 我們再來看s3,s3則直接在堆中創建"aa",第一個"a",intern原本是要將第一個遇見的"a"放入常量池的,但是常量池中已經存在"a"了,於是便不會管,new 的第二個"a"也不會管,但是到"aa"的時候,發現常量池中並沒有"aa",於是,直接將s3的引用放入常量池,而不是副本,這樣s4在創建的時候,發現常量池中有引用,便直接指向引用,而該引用是指向堆中的s3,故結果爲true。

jdk1.6下結果

false
false

第一個false跟上邊的一樣,第二個false是因爲jdk1.6的intern()發現常量池中沒有"aa",則直接將此字符串對象添加到常量池中,兩個"aa"的地址是不一樣的,一個是堆中的一個是常量池中的,故結果也是false。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章