1. 程式人生 > >JVM(三):深入分析Java位元組碼-上

JVM(三):深入分析Java位元組碼-上

JVM(三):深入分析Java位元組碼-上

位元組碼文章分為上下兩篇,上篇也就是本文主要講述class檔案存在的意義,以及其帶來的益處。並分析其內在構成之一 ———位元組碼,而下篇則從指令集方面著手,講解指令集都有哪些,以及其各自代表的含義。最後總結一下Class檔案存在的必然性。

意義

前面說過 Java 虛擬機器擁有平臺無關性,但其實現在語言無關性在 JVM 和更加的體現了出來。表現就是目前越來越多的語言可以在 JVM 上執行,而這背後的邏輯,就是這些語言都會被編譯為 Class 檔案,然後在JVM上 執行。

In the future,we will consider bounded extensions to the java virtual machine to provide better support for other languages.

上面這段是 JVM 的相關人員提出的願景,因此我們也對 Java 語言的發展更加的看好。

那麼在下面的文章中,我們就來探討 Class 檔案的組成部分,瞭解其內部是如何組織的。

Class類檔案

首先我們編寫一個原始的Java原始碼:

public class TestForEach extends Thread{
private static int ccc = 1;
public static void main(String[] args) {

   int a = 1;
   int b = 2;
   int c = a + b;
}

這裡我們使用JDk提供的工具對程式碼進行編譯,得到下面這個二進位制流。

    cafe babe 0000 0034 001d 0a00 0600 0f09
    0010 0011 0800 120a 0013 0014 0700 1507
    0016 0100 063c 696e 6974 3e01 0003 2829
    5601 0004 436f 6465 0100 0f4c 696e 654e
    756d 6265 7254 6162 6c65 0100 046d 6169
    6e01 0016 285b 4c6a 6176 612f 6c61 6e67
    2f53 7472 696e 673b 2956 0100 0a53 6f75
    .........

對我們來說,所需要分析的就是這個檔案。該二進位制流的前4個位元組cafe babe

,其被稱為魔數。它代表了這是一個.class型別的檔案,緊接著的第五第六個位元組為次版本號,第七第八個位元組為主版本號,而我們編譯的這個版本是在 JDK1.8 下。再緊接著就是常量池,訪問標示等資訊,因為這些資訊計算十分的繁瑣麻煩,在這裡就不展開來計算了,而官方也細心地提供了javap 工具進行反編譯來檢視其組織形式。

位元組碼檔案

首先我們採用javap工具進行反編譯javap -verbose TestForEach,得到如下檔案

      Last modified 2019-5-22; size 461 bytes
      MD5 checksum 2602dfd883d5d5e417e26ce2d42b916d
      Compiled from "TestForEach.java"
    public class TestForEach extends java.lang.Thread
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#22         // java/lang/Thread."<init>":()V
       #2 = Fieldref           #5.#23         // TestForEach.d:I
       #3 = Fieldref           #5.#24         // TestForEach.dfinal:I
       #4 = Fieldref           #5.#25         // TestForEach.ccc:I
       #5 = Class              #26            // TestForEach
       #6 = Class              #27            // java/lang/Thread
       #7 = Utf8               ccc
       #8 = Utf8               I
       #9 = Utf8               d
      #10 = Utf8               dfinal
      #11 = Utf8               ConstantValue
      #12 = Integer            2222
      #13 = Utf8               <init>
      #14 = Utf8               ()V
      #15 = Utf8               Code
      #16 = Utf8               LineNumberTable
      #17 = Utf8               main
      #18 = Utf8               ([Ljava/lang/String;)V
      #19 = Utf8               <clinit>
      #20 = Utf8               SourceFile
      #21 = Utf8               TestForEach.java
      #22 = NameAndType        #13:#14        // "<init>":()V
      #23 = NameAndType        #9:#8          // d:I
      #24 = NameAndType        #10:#8         // dfinal:I
      #25 = NameAndType        #7:#8          // ccc:I
      #26 = Utf8               TestForEach
      #27 = Utf8               java/lang/Thread
    {
      public TestForEach();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Thread."<init>":()V
             4: aload_0
             5: iconst_1
             6: putfield      #2                  // Field d:I
             9: aload_0
            10: sipush        2222
            13: putfield      #3                  // Field dfinal:I
            16: return
          LineNumberTable:
            line 11: 0
            line 17: 4
            line 18: 9
    
      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_2
             1: istore_2
             2: iconst_1
             3: iload_2
             4: iadd
             5: istore_3
             6: return
          LineNumberTable:
            line 22: 0
            line 23: 2
            line 111: 6
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: iconst_1
             1: putstatic     #4                  // Field ccc:I
             4: return
          LineNumberTable:
            line 16: 0
    }
    SourceFile: "TestForEach.java"

檔案頭

我們從頭開始看起,首先是

      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER

這裡分別為Class檔案的次版本和主版本號以及訪問標示,版本號前面已經說過了,這裡就不多贅述了,至於訪問標誌,其代表了一些類或介面的訪問資訊,如是否是public型別的,有沒有被宣告成final等等。

常量池

接著到了 ConstantPool常量池,其可以簡單理解為Class檔案中的資源倉庫,許多部分都與其關聯。

其中存放了字面量和符號引用兩種類常量。字面量可以理解為文字字串和宣告為final的常量值等。符號引號則包含以下內容:

  • 類和介面的全限定名;
  • 欄位的名稱和描述符;
  • 方法的名稱和描述符。

上面兩種型別在常量池中都是以表的形式來儲存,其具體含義如下圖所示:

針對常量池內每個表的含義和我們得到的class檔案,在這作者分析幾個看一下:

  • Methodref:方法的符號引用,具體內容跳到6和22行,但從後面的註釋可以看到是父類Thread的構造方法
  • Fieldref:欄位的符號引用,具體內容在5,23行,可以看到是在TestForEach類中定義了int型別的屬性d
  • Class:類或介面的符號引用,從中可以看出該檔案是TestForEach類,繼承自Thread
  • Utf8:表明其是一個字串,在檔案中欄位描述ccc,d,dfinal,全限定名java/lang/Thread等,方法描述main,()V等都屬於這一類
  • NameAndType:欄位或方法的符號引用,與上面不同的是,其沒有宣告是哪一個類的
  • .....

更多的這裡就不詳細展開了,但常量池中還有一些上面沒有提到的內容,在這裡我們細說一下.

屬性表集合

屬性表集合用於描述某些場景中專有的資訊.針對上文出現的屬性表,我們這裡詳細說下.

  • ConstantValue:final關鍵字定義的常量值;
  • Code:Java程式碼編譯而成位元組碼指令,具體指令含義在下篇中細說;
  • LineNumberTable:Java原始碼行號和位元組碼指令的對應關係;
  • SourceFile:原始檔的名稱
  • .........

總結

本文講了 Class 檔案在 Java 達成平臺無關性和語言無關性起到的重要作用,並敘述了Class檔案的重要組成部分----檔案標識,常量池,屬性集合等。此外還對檔案標識和常量池的內容進行了具體的展開描述。

下篇文章則從 JVM 支援的指令集內容開始介紹,並以本文的 Class 檔案的指令集集合為例,具體描述一下指令集的內容。

文章在公眾號"iceWang"第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯。

本系列文章主要借鑑自《深入分析JavaWeb技術內幕》和《深入理解Java虛擬機器-JVM高階特性與最佳實踐》。