JVM--剖析類與對象在JVM中從生存至死亡

前面學習了Class文件結構、類的加載機制、字節碼執行引擎、對象的創建與銷燬,所以我準備從一個Java代碼進行切入,詳細剖析它的生命歷程,將所學的知識真正的用起來,也算是對前面所學的知識進行一個系統的總結。


我們以這份Java代碼爲例,來剖析一個Java程序的生命歷程:

interface ClassName {
    String getClassName();
}

class Company implements ClassName {
    String className;

    public Company(String className) {
        this.className = className;
    }

    @Override
    public String getClassName() {
       return className;
    }
}

public class Main {
    public static void main(String[] args) {
        String className;

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            className = scanner.next();
            Company company = new Company(className);
            System.out.println("name=" + company.getClassName());
        }
    }
}

可以看到,這份代碼涉及到了接口,繼承,對象的實例化,main方法,值得我們花費一些功夫去從JVM層面上了解這個程序從編譯、運行到結束都發生了哪些事情。

所以,別急,讓我們按順序慢慢來分析。


編譯階段

首先你要運行一個java程序,肯定要對其進行編譯,生成我們前面說的Class文件,這段代碼會生成3個Class文件。

Class文件中保存了魔數、版本符號、常量池、方法標誌、類索引、父類索引、接口索引、字段表(有可能含有屬性表)、方法表(有可能含有屬性表)等信息。這些信息具體的組成結構,我在這裏不再贅述。

我們可以通過字節碼文件,清晰的描述出Java源碼中有關類的所有信息。

在這裏只以Main類爲例,使用javap命令看一下生成的Class文件。

javap -verbose Main;
  Last modified 2017-12-13; size 852 bytes
  MD5 checksum 0336fa14cc04a9c858c34cc016880c19
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#29        // java/lang/Object."<init>":()V
   #2 = Class              #30            // java/util/Scanner
   #3 = Fieldref           #31.#32        // java/lang/System.in:Ljava/io/InputStream;
   #4 = Methodref          #2.#33         // java/util/Scanner."<init>":(Ljava/io/InputStream;)V
   #5 = Methodref          #2.#34         // java/util/Scanner.hasNext:()Z
   #6 = Methodref          #2.#35         // java/util/Scanner.next:()Ljava/lang/String;
   #7 = Class              #36            // Company
   #8 = Methodref          #7.#37         // Company."<init>":(Ljava/lang/String;)V
   #9 = Fieldref           #31.#38        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Class              #39            // java/lang/StringBuilder
  #11 = Methodref          #10.#29        // java/lang/StringBuilder."<init>":()V
  #12 = String             #40            // name=
  #13 = Methodref          #10.#41        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #14 = Methodref          #7.#42         // Company.getClassName:()Ljava/lang/String;
  #15 = Methodref          #10.#43        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #17 = Class              #46            // Main
  #18 = Class              #47            // java/lang/Object
  #19 = Utf8               <init>
  #20 = Utf8               ()V
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               StackMapTable
  #26 = Class              #30            // java/util/Scanner
  #27 = Utf8               SourceFile
  #28 = Utf8               Main.java
  #29 = NameAndType        #19:#20        // "<init>":()V
  #30 = Utf8               java/util/Scanner
  #31 = Class              #48            // java/lang/System
  #32 = NameAndType        #49:#50        // in:Ljava/io/InputStream;
  #33 = NameAndType        #19:#51        // "<init>":(Ljava/io/InputStream;)V
  #34 = NameAndType        #52:#53        // hasNext:()Z
  #35 = NameAndType        #54:#55        // next:()Ljava/lang/String;
  #36 = Utf8               Company
  #37 = NameAndType        #19:#56        // "<init>":(Ljava/lang/String;)V
  #38 = NameAndType        #57:#58        // out:Ljava/io/PrintStream;
  #39 = Utf8               java/lang/StringBuilder
  #40 = Utf8               name=
  #41 = NameAndType        #59:#60        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #61:#55        // getClassName:()Ljava/lang/String;
  #43 = NameAndType        #62:#55        // toString:()Ljava/lang/String;
  #44 = Class              #63            // java/io/PrintStream
  #45 = NameAndType        #64:#56        // println:(Ljava/lang/String;)V
  #46 = Utf8               Main
  #47 = Utf8               java/lang/Object
  #48 = Utf8               java/lang/System
  #49 = Utf8               in
  #50 = Utf8               Ljava/io/InputStream;
  #51 = Utf8               (Ljava/io/InputStream;)V
  #52 = Utf8               hasNext
  #53 = Utf8               ()Z
  #54 = Utf8               next
  #55 = Utf8               ()Ljava/lang/String;
  #56 = Utf8               (Ljava/lang/String;)V
  #57 = Utf8               out
  #58 = Utf8               Ljava/io/PrintStream;
  #59 = Utf8               append
  #60 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #61 = Utf8               getClassName
  #62 = Utf8               toString
  #63 = Utf8               java/io/PrintStream
  #64 = Utf8               println
{
  public Main();
    descriptor: ()V
    flags: 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 27: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class java/util/Scanner
         3: dup
         4: getstatic     #3                  // Field java/lang/System.in:Ljava/io/InputStream;
         7: invokespecial #4                  // Method java/util/Scanner."<init>":(Ljava/io/InputStream;)V
        10: astore_2
        11: aload_2
        12: invokevirtual #5                  // Method java/util/Scanner.hasNext:()Z
        15: ifeq          63
        18: aload_2
        19: invokevirtual #6                  // Method java/util/Scanner.next:()Ljava/lang/String;
        22: astore_1
        23: new           #7                  // class Company
        26: dup
        27: aload_1
        28: invokespecial #8                  // Method Company."<init>":(Ljava/lang/String;)V
        31: astore_3
        32: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: new           #10                 // class java/lang/StringBuilder
        38: dup
        39: invokespecial #11                 // Method java/lang/StringBuilder."<init>":()V
        42: ldc           #12                 // String name=
        44: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        47: aload_3
        48: invokevirtual #14                 // Method Company.getClassName:()Ljava/lang/String;
        51: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        54: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        57: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        60: goto          11
        63: return
      LineNumberTable:
        line 31: 0
        line 32: 11
        line 33: 18
        line 34: 23
        line 35: 32
        line 36: 60
        line 37: 63
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 11
          locals = [ top, class java/util/Scanner ]
        frame_type = 51 /* same */
}
SourceFile: "Main.java"

我們大概分析一下上面的輸出結果吧:

直接從常量池開始分析,前面的幾行信息我認爲沒有分析的必要。

   #1 = Methodref          #18.#29        // java/lang/Object."<init>":()V

這是常量池裏面的第一項數據,#1代表索引,Methodref告訴我們常量池中第一個索引表示的是一個方法引用,對這個方法引用的描述用常量池中第18項和第29項的內容可以進行描述,你們可以查一下常量池中第18項和第29項的內容,其實對應的就是後面註釋的內容。它告訴了你這個方法所屬的類是Object,方法的簡單名稱是<init>,方法的描述符是()V,也就是Object中的實例構造函數(對簡單名稱和描述符我在前面的博客中已經進行了說明)。

你也許想問,爲什麼Main類常量池中的第一項數據描述的是Object類中的無參構造函數?你可能忘了,所有類都應如此,Java中所有的類都繼承自Object,常量池中也會保存他們父類的索引,因爲在Java中,對象的初始化與實例化不是還有一條規則嘛—先初始化與實例化父類,然後纔是子類(說的並不精確,明白我的意思就行)。

剩餘的常量池分析與上面類似。

常量池下面的代碼塊,可以看到,一個Main類的默認構造函數,一個就是main方法了。關於這兩個東西,我們等一下在字節碼的執行階段再說。

可以看到,Class文件中,包含着詳細的信息,有大量的信息都是你無法從源碼中直接得到的。


類加載階段

javac對源文件編譯完成,我們使用java命令開始運行這個Main類。java命令只能運行包含main方法的類

java命令一開始運行,JVM開始對Main類進行加載

這時候就對應了我們學習類加載機制的每個階段:加載(從.class文件中讀取字節碼)、驗證(對字節碼文件進行一系列的驗證保證格式無誤並且對JVM不會產生危害)、準備(爲類變量分配內存並進行系統初始化)、解析(分爲靜態鏈接與動態鏈接,非虛方法的符號引用會在這一階段被解析爲直接引用)、初始化(執行類構造器)。

我再對其過程進行一點點補充,如果想要更加詳細的說明,請移步至我的博客專欄。

JVM在加載這個Main類的時候,使用類加載器(雙親委派模型)對其進行加載,經歷了加載、驗證(在這個時候又會觸發Object類的加載)階段,由於在Main類中並沒有類變量,也就相當於跳過了準備這一階段,然後對字節碼進行解析,由於main方法是靜態方法,也就是非虛方法,開始靜態鏈接,在字節碼中直接將main方法的符號引用解析爲直接引用(別忘了Main類反編譯之後的默認構造器~),由於沒有靜態變量與靜態語句塊,所以初始化這一階段也相當於是直接跳過,最後整個加載過程完畢,並在方法區中生成Main類所對應的Class對象

由於我這個例子中不涉及多態,也就不涉及分派,但這部分知識請務必掌握。


方法執行階段

類中所有的信息已經在內存中加載完畢,JVM開始進行方法調用… …

方法調用:JVM開始執行main方法,這部分工作是由虛擬機中字節碼執行引擎完成的。main方法會由一個線程進行調用。此線程會在虛擬機棧上爲自己開闢一部分的棧空間,此後只要這個線程調用新的方法,這個方法便會被當作棧幀壓入虛擬機棧的棧頂(作爲當前棧幀)。這個方法中定義的局部變量會被存儲進局部變量表,在JVM中,並不存儲局部變量的名稱,他們都是以局部變量表的相對偏移量來標識每個不同的局部變量。

我以main方法的Code屬性再說明一下棧幀中的局部變量表以及操作數棧。(Class文件中方法表的Code屬性保存的是Java方法體中的字節碼)

Code:
      stack=3, locals=4, args_size=1

可以看到,main方法在調用之前(實際上在編譯階段,它的局部變量表,操作數棧的大小都已確定),locals爲4,也就是局部變量表的大小爲4:this,className,scanner,company;stack爲3,也就是操作數棧的大小3:className,scanner,company。

我們還可以在上面方法體的字節碼當中看到許多指令,而這些指令就是方法在執行的過程中,JVM需要解釋運行的,如下:

0: new           #2                  // class java/util/Scanner

前面的0表示的也是相對偏移量,而new指令就是新建一個對象,對應Java源碼中的new,後面的#2代表的是new指令的參數,表達的意思是常量池中索引爲2的數據項,也就是上述代碼後面註釋中的Scanner類。

值得一說的是,這裏的Scanner由於是對類的實例化,因此JVM會首先判斷Scanner這個類是否會被加載進內存,如果沒有被加載進內存,JVM開始對這個類進行類加載,過程如上述步驟,然後進行對象的初始化。

指令一條條的向下面執行(程序計數器),進入循環體,最終執行到這一步:Company company = new Company(className);,也是一個對類的實例化,但它和上面的Scanner又有點不同,這個類不僅實現了ClassName接口,而且實例化的時候還進行了傳參。那麼何時會加載ClassName接口呢,博主根據查閱的資料,猜測是在Company類加載中的驗證階段會觸發其接口的加載,具體大家可以Google、baidu。

那麼如何實現參數的傳遞呢,相信大家應該是有印象的,它是在實例化Company類的過程中,執行了Company的構造方法,而構造方法本身又是一個方法,因此可以理解爲實參先進入被調用方法的操作數棧中,然後將操作數棧中的引用出棧賦值給被調用方法的局部變量表。

最後,代碼執行完畢,程序退出。


參考閱讀

JVM 內部原理(六)— Java 字節碼基礎之一

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