jvm學習 java代碼的運行過程

系統性學習jvm請點擊jvm學習目錄

前言

在之前,我們講了java內存佈局,講了垃圾收集,將了字節碼文件,其實這些東西都很抽象,看了之後也很難和具體我們寫的代碼相聯繫起來,相信很多人其實都更關心自己的代碼是如何運行的。希望對java代碼的運行有一個直觀的瞭解。
那麼本篇博客就對java代碼的整個運行過程進行一個簡單的介紹,讓讀者對jvm能有一個更清晰的把握,不至於一上來就懵逼。

從java代碼到運行

下面我用一個例子來介紹java代碼從誕生到運行的過程吧。

首先我們開始編寫java代碼,爲了方便,編寫個比較簡單的吧。

public class Test {
    public int calc() {
        int a = 12;
        int b = 34;
        return a + b;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.calc();
    }
}

如上所示一個簡單的java代碼。


在編寫完代碼之後,就是對代碼進行編譯,將java代碼Test類轉換成類Class文件,只有這個文件,jvm才能夠讀取。
這個步驟是編譯器完成的。
編譯器將.java文件編譯成類class文件。
其中類class文件的情況如下所示。

Classfile /H:/Test.class
  Last modified 2020531; size 358 bytes
  MD5 checksum 6758967fd1d4328a6721de106d960e46
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 53
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // Test
  super_class: #5                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // Test
   #3 = Methodref          #2.#16         // Test."<init>":()V
   #4 = Methodref          #2.#18         // Test.calc:()I
   #5 = Class              #19            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               calc
  #11 = Utf8               ()I
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               Test.java
  #16 = NameAndType        #6:#7          // "<init>":()V
  #17 = Utf8               Test
  #18 = NameAndType        #10:#11        // calc:()I
  #19 = Utf8               java/lang/Object
{
  public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public int calc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        12
         2: istore_1
         3: bipush        34
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 6

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Test
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method calc:()I
        12: pop
        13: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 13
}
SourceFile: "Test.java"

這裏如果對類class文件(字節碼文件)情況不清楚的可以點擊jvm學習 字節碼文件(Class類文件)結構介紹及快速理解


當有了類class文件之後,並做不了什麼,因爲此時Test類的信息還是在硬盤中存儲的。
當我們鍵入 java Test時,jvm便開始運行Test.class文件了。
此時需要將這個Test類加載到內存中去。該過程就是類加載過程。
jvm使用類加載器將Test類的信息加載到內存中去。(類加載之後我會專門寫)。
類加載器會將整個類class文件的核心常量池中的東西放到java內存中的運行時常量池中。將方法表集合關於方法的字節碼指令放到方法區中去。


下面,在運行時。從main作爲程序的進入點。
首先將main函數放進該線程的虛擬機棧,作爲第一個棧幀,然後根據方法表集合中的stack和locals來分配操作數棧和局部變量表。
上面類class文件中main()方法中code屬性下第一行就是:在這裏插入圖片描述
那麼jvm就會給這個main函數棧幀分配一個深度爲2的操作數棧,然後其中的局部變量表大小爲2個變量槽。


當該分配完的東西分配完了之後,jvm將會執行棧頂的方法,也就是執行main方法。(main方法不詳細講,下面詳細的將calc方法)首先它創建一個Test類的實例對象test,然後調用test對象的calc()方法。
此時main方法中斷,保存當前運行現場。
calc()方法進入虛擬機棧,稱爲棧頂。
jvm此時開始運行calc()方法。


下面來結合字節碼指令詳細講一下calc()方法。
calc()方法的字節碼指令如下圖所示:
在這裏插入圖片描述
可以看到,該方法操作數棧深度爲2,局部變量表大小爲3個變量槽。
(字節碼指令是基於棧的指令,之前本科接觸過彙編指令,那其實是基於寄存器的指令,彙編指令可以將數據存儲在指定的寄存器中,每個指令基本都可以包含兩個輸入參數:寄存器和數值,例:mov eax 2。而字節碼指令基本沒有輸入,例:iconst 和2兩條指令是:將2壓入棧。所以字節碼指令的計算操作都是圍繞這操作數棧來進行。)
下面來介紹calc()方法具體執行過程。

  • 首先開始執行前兩行指令,bipush 12是將12這個棧頂壓入操作數棧。注意,這裏可以看到是第0行指令之後便是第2行指令了。所以這裏bipush
    12實際是2個指令,本身字節碼指令也是零地址指令,所以不要弄錯了。第0個指令是bipush,代表將之後一條指令存儲的數據放入操作數棧。第1個指令代表要存入操作數棧的數是12。

  • 第2個指令istore_1以爲將操作數棧中的棧頂數據出棧,放入局部變量表的第一個變量槽存儲。所以12出棧,放入局部變量表槽1,操作數棧棧空。

  • 同理,3,4指令就是34進棧。

  • 同理,5指令是34存入局部變量表槽2。經過這些操作,操作數棧又空了。

  • 第6個指令是iload_1是將局部變量表槽1內容12複製到操作數棧。

  • 同理,第7個指令是將局部變量表槽2內容34複製到操作數棧。

  • 第8個指令iadd意爲將操作數棧中的兩個棧頂元素出棧,並相加,並將結果再存入操作數棧。

  • 第9個指令就很好理解了。返回棧頂元素。


之後calc()方法就執行完了。calc()方法棧幀出棧。
之後main方法又稱爲棧頂。在之前中斷的地方繼續往下執行。最終執行完畢。
整個java代碼的一生就可以認爲是結束了。

總結

總結一下,整個過程基本就是這樣的流程:java代碼–字節碼文件–加載進內存–方法進棧–執行–出棧。
在字節碼指令時,一定是圍繞着操作數棧來實現的,當然局部變量表的作用也很大。

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