JAVA是如何運行的一JVM模型

關於JAVA是如何運行的一直很模糊 今天來總結下

一,首先先理解幾個基本概念:

  • JDK(Java Development Kit),Java開發工具包,主要用於開發
  • JRE(Java Runtime Environment),Java程序運行的核心環境,包括JVM和一些核心庫
  • JVM(Java Virtual Machine),VM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的,是JRE核心模塊。

二 ,我們要知道運行時java會做什麼事

      首先任何.java文件都會被javaC.exe 編譯成JVM可以運行的.class文件

Main.java

package com.company;

public class Test1 {

    static int n = 5;

    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
        System.out.println(c);
        getN();
    }

    public static void getN(){
        n++;
        System.out.println(n);
    }
}

Main.class

00000000| CA FE BA BE 00 00 00 34 00 1D 0A 00 04 00 0F 09 |.......4........
00000010| 00 10 00 11 08 00 12 07 00 13 0A 00 14 00 15 07 |................
00000020| 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 |.....<init>...()
00000030| 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E |V...Code...LineN
00000040| 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 |umberTable...mai
00000050| 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 |n...([Ljava/lang
00000060| 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 |/String;)V...Sou
00000070| 72 63 65 46 69 6C 65 01 00 09 4D 61 69 6E 2E 6A |rceFile...Main.j
00000080| 61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 19 |ava.............
00000090| 01 00 0B 48 65 6C 6C 6F 20 57 6F 72 6C 64 01 00 |...Hello.World..
000000A0| 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 |.java/lang/Objec
000000B0| 74 07 00 1A 0C 00 1B 00 1C 01 00 10 63 6F 6D 2F |t...........com/
000000C0| 63 6F 6D 70 61 6E 79 2F 4D 61 69 6E 01 00 10 6A |company/Main...j
000000D0| 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 |ava/lang/System.
000000E0| 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F |..out...Ljava/io
000000F0| 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 |/PrintStream;...
00000100| 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 |java/io/PrintStr
00000110| 65 61 6D 01 00 06 70 72 69 6E 74 66 01 00 3C 28 |eam...printf..<(
00000120| 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E |Ljava/lang/Strin
00000130| 67 3B 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 |g;[Ljava/lang/Ob
00000140| 6A 65 63 74 3B 29 4C 6A 61 76 61 2F 69 6F 2F 50 |ject;)Ljava/io/P
00000150| 72 69 6E 74 53 74 72 65 61 6D 3B 00 21 00 06 00 |rintStream;.!...
00000160| 04 00 00 00 00 00 02 00 01 00 07 00 08 00 01 00 |................
00000170| 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 |.............*..
00000180| 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 |................
00000190| 00 03 00 09 00 0B 00 0C 00 01 00 09 00 00 00 2A |...............*
000001A0| 00 03 00 01 00 00 00 0E B2 00 02 12 03 03 BD 00 |................
000001B0| 04 B6 00 05 57 B1 00 00 00 01 00 0A 00 00 00 0A |....W...........
000001C0| 00 02 00 00 00 07 00 0D 00 08 00 01 00 0D 00 00 |................
000001D0| 00 02 00 0E                                     |....

 

一般來說流程入下:

JVM 會將字節碼讀取至運行時數據區,然後從main方法開始執行字節碼。執行過程如下:

三,通過上圖,可以看到ClassLoad會將Class文件 以及用到的java api 讀取到數據區,然後根據不同的內容將數據存儲到不同的內存區域。

運行時數據區可大致分爲:方法區,堆區,棧區,PC寄存器區和本地方法棧區,這也就是常說的JAVA內存結構。

JVM內存模型

 

 

JVM 將內存劃分爲 5個部分,分別爲線程共享的 方法區,以及線程私有的 程序計數器虛擬機棧本地方法棧,下面就讓我們針對這 5個區域進行學習,探究其存儲數據,生命週期和功能。

1,程序計數器

是一塊較小的內存區域,可以看做是當前線程執行的字節碼的行號指示器。在虛擬機概念模型裏,字節碼解析器就是通過改變這個計數器的值來選取下一條需要執行的字節碼,因此其在分支,循環,跳轉,異常跳轉,線程恢復等功能上都有着大作用。

PS:如果執行的是本地方法,那麼這個計數器的值則爲空。

2,虛擬機棧

虛擬機棧也是線程私有的,其內描述的是 Java 方法執行的內存模型,即在每個執行同時創建一個棧幀,棧幀內存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法從開始到結束就對應着一個棧幀從入棧到出棧的過程。同時只有位於棧頂的棧幀纔是有效的,與其關聯的方法稱爲當前方法,執行引擎的所有字節碼指令都只針對當前棧幀進行操作。

2.1 局部變量表

用於存放方法參數和方法內部定義的局部變量,其在 Java 程序被編譯爲 Class 文件後,就已經確定了所需的最大容量。

其容量以變量槽(slot)爲最小單位。因此在使用過程中是通過索引定位來使用局部變量表的,索引範圍爲 0~~slot 最大值。其中如果執行的是非 static 方法,那麼0則默認爲 方法所屬對象實例引用,對應 Java 關鍵字的 this。其餘參數按照順序對應 1之後的槽位。

2.2 操作數棧

操作棧是一個後入先出的棧,其最大深度在編譯時也已經確定。其對應着方法執行過程中,各種字節碼指令往操作數棧寫入和提取內容,也就是所謂的 入棧/出棧 操作。

也正是操作數棧的存在,因此Java執行引擎也被稱爲 基於棧的執行引擎,與基於 基於寄存器的執行引擎 形成對比。

Java採取「基於棧的執行引擎」考慮到兩點:

  1. Java是一門跨平臺的語言,而不同機器的寄存器實現是不同的,有多又少,不利於統一;
  2. 爲了使 class 文件更加的緊湊,這樣設計可以使得大多數指令對齊,並且操作碼只佔一個字節大小,減少數據量。

2.3 動態連接

指向運行時常量池中該棧幀所屬方法的引用,通過這個引用可以完成動態調用。

關於方法調用過程中的引用詳細解析過程,在日後的「方法調用」中,再具體描述。

2.4 返回地址

一個方法在執行完成後都需要返回到方法被調用的位置,讓程序繼續執行。

在方法正常執行完成退出後,調用者的程序計數器的值就可以作爲返回地址存在棧幀中,而在方法異常退出後,返回地址則是通過異常處理器表來確定了。

2.5 附加信息

附加信息不是虛擬機規範中必須要求有的,但其允許實現者可以增加一些特殊信息到棧幀中,例如與調試相關的信息,這部分信息取決於具體的虛擬機實現,在這裏不再贅述。

三,本地方法棧

本地方法棧和虛擬機棧的作用類似,區別僅僅是虛擬機棧爲虛擬機執行的 Java 方法服務,而本地方法棧則是爲 Native 方法服務。其具體實現由虛擬機自行規定。

四,堆

Java 堆是線程共享的。在一般情況下,堆可以說是 Java 內存中最大的內存區域。其存放了對象實例,幾乎所有的對象實例在這裏存儲。(這裏說是幾乎,是因爲 JIT優化的存在,可能會有對象不在堆上分配,而在棧上進行分配)。

由於目前考慮到垃圾回收算法大部分都是分代算法,因此堆又可以細分爲以下幾塊:

 

五,方法區

方法區也是線程共享的。其中存放的是被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。在HotSpot JDK7 以前的具體實現中,這部分被稱爲永久代,和堆一起 JVM 管理。但在JDK8之後,這部分已經用 元數據(meta space) 來替代了。此外像字符串常量池也被從這一模塊移除,轉而用堆來實現。

 

上述介紹完之後 我們再來看.class文件是怎麼操作的

.class文件我們很難看懂 不過可以使用javap指令進行反編譯 (密集恐懼症勿入)

Classfile /C:/Users/maxc/Desktop/Test/src/com/company/Test1.class
  Last modified 2020-5-6; size 580 bytes
  MD5 checksum 54acc5781fba7440ba0aae6f9652f592
  Compiled from "Test1.java"
public class com.company.Test1
  minor version: 0   //最低版本號
  major version: 52 //版本號
  flags: ACC_PUBLIC, ACC_SUPER //標識靜態 公共方法
Constant pool:  //常量池

# #1=Methodref  等號簽名是常量池的編號,從1開始編號,每個常量項佔一個編號。等號後面的是該常量項的類型(jvm定義的14中類型)。

#  第二列。如果常量項是一個引用,指向其他的常量項,那地三列就是該向量項指向那個常量項的編號。

#第3列就是常量項的符號引用。全類名/字段名/方法名,以及字段或者方法的描述符。

# 比如第一行:第一個常量項是CONSTANT_Methodref_info類型的,它表示一個方法的常量引用,它的就結構中有兩個index:

#第一個是指向該方法的類描述符的常量項(CONSTANT_Class_info)。第二個index是該方法名稱及類型的描述符(CONSTANT_NameAndType)

#依次往下找,找到一個完整的方法相關的常量

#第3列就是符號應用:java.lang.Object.<init>的構造方法,()V方法返回值是void,沒有形參表。


   #1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #23.#24        // java/io/PrintStream.println:(I)V
   #4 = Methodref          #6.#25         // com/company/Test1.getN:()V
   #5 = Fieldref           #6.#26         // com/company/Test1.n:I
   #6 = Class              #27            // com/company/Test1
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               n
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               getN
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               Test1.java
  #20 = NameAndType        #10:#11        // "<init>":()V
  #21 = Class              #29            // java/lang/System
  #22 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #23 = Class              #32            // java/io/PrintStream
  #24 = NameAndType        #33:#34        // println:(I)V
  #25 = NameAndType        #16:#11        // getN:()V
  #26 = NameAndType        #8:#9          // n:I
  #27 = Utf8               com/company/Test1
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  static int n;
    descriptor: I
    flags: ACC_STATIC  //定義靜態變量n

  public com.company.Test1(); //構造器?
    descriptor: ()V
    flags: ACC_PUBLIC //定義公共方法
    Code:
      stack=1, locals=1, args_size=1  //棧深度爲1 局部變量數1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1  //向操作數棧壓入1
         1: istore_1  //將值壓出操作棧 並存儲爲索引爲1的變量中 相當於 a = 1
         2: iconst_2  //向操作數棧壓入2
         3: istore_2  //將值壓出操作棧 並存儲爲索引爲2的變量中 相當於 b = 2
         4: iload_1   //向操作數棧壓入索引爲1的變量值
         5: iload_2   //向操作數棧壓入索引爲2的變量值
         6: iadd      //將操作棧的值相加
         7: istore_3    //將值壓出操作棧 並存儲爲索引爲3的變量中 相當於 c = 1
         8: getstatic     #2  //獲取流方法                // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_3  //向操作數棧壓入索引爲3的變量值
        12: invokevirtual #3   //打印               // Method java/io/PrintStream.println:(I)V
        15: invokestatic  #4    //調用方法getN              // Method getN:()V
        18: return
      LineNumberTable: //Java源碼行號和字節碼指令的對應關係
        line 8: 0
        line 9: 2
        line 10: 4
        line 11: 8
        line 12: 15
        line 13: 18

  public static void getN();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #5  //讀取靜態數n = 5                // Field n:I
         3: iconst_1 //向操作數棧壓入1
         4: iadd //將操作數棧數值相加
         5: putstatic     #5   //設置n = 6      // Field n:I
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: getstatic     #5                  // Field n:I
        14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 18: 17

  static {}; //靜態方法
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_5  //向操作數棧壓入5
         1: putstatic     #5   //設置n = 5               // Field n:I
         4: return
      LineNumberTable:
        line 5: 0
}

多試試 ,this all。

 

 

 

 

 

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