博客 / 詳情

返回

類字節碼:揭開Java虛擬機運行機制的神秘面紗

概述

計算機是不能直接運行java代碼的,必須要先運行java虛擬機,再由java虛擬機運行編譯後的java代碼。

因為在cpu層面看來計算機中所有的操作都是一個個指令的運行彙集而成的,java是高級語言,只有人類才能理解其邏輯,計算機是無法識別的,所以java代碼必須要先編譯成字節碼文件,jvm才能正確識別代碼轉換後的指令並將其運行。

Java代碼間接翻譯成字節碼,儲存字節碼的文件再交由運行於不同平台上的JVM虛擬機去讀取執行,從而實現一次編寫,到處運行的目的。

Java字節碼文件

class文件本質上是一個以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊的排列在class文件中。jvm根據其特定的規則解析該二進制數據,從而得到相關信息。

Class文件的結構屬性

反編譯字節碼文件

javac 生成字節碼文件,javap 反編譯字節碼文件

javap用法

javap <options> <classes>

 -help  --help  -?        輸出此用法消息
  -version                 版本信息
  -v  -verbose             輸出附加信息
  -l                       輸出行號和本地變量表
  -public                  僅顯示公共類和成員
  -protected               顯示受保護的/公共類和成員
  -package                 顯示程序包/受保護的/公共類
                           和成員 (默認)
  -p  -private             顯示所有類和成員
  -c                       對代碼進行反彙編
  -s                       輸出內部類型簽名
  -sysinfo                 顯示正在處理的類的
                           系統信息 (路徑, 大小, 日期, MD5 散列)
  -constants               顯示最終常量
  -classpath <path>        指定查找用户類文件的位置
  -cp <path>               指定查找用户類文件的位置
  -bootclasspath <path>    覆蓋引導類文件的位置

反編譯

//Main.java
public class Main {
    
    private int m;
    
    public int inc() {
        return m + 1;
    }
}

對以上例子輸入命令javap -verbose -p Main.class查看輸出內容:

Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
  Last modified 2018-4-7; size 362 bytes
  MD5 checksum 4aed8540b098992663b7ba08c65312de
  Compiled from "Main.java"
public class com.rhythm7.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/rhythm7/Main.m:I
   #3 = Class              #20            // com/rhythm7/Main
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/rhythm7/Main;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               Main.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/rhythm7/Main
  #21 = Utf8               java/lang/Object
{
  private int m;
    descriptor: I
    flags: ACC_PRIVATE

  public com.rhythm7.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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/rhythm7/Main;

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/rhythm7/Main;
}
SourceFile: "Main.java"

字節碼文件信息

開頭的7行信息包括:Class文件當前所在位置,最後修改時間,文件大小,MD5值,編譯自哪個文件,類的全限定名,jdk次版本號,主版本號。

然後緊接着的是該類的訪問標誌:ACC_PUBLIC, ACC_SUPER,訪問標誌的含義如下:

常量池

Constant pool意為常量池。

主要存放的是兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量類似於java中的常量概念,如文本字符串,final常量,而符號引用則屬於編譯原理方面的概念,包括以下三種:

  • 類和接口的全限定名(Fully Qualified Name)

  • 字段的名稱和描述符號(Descriptor)

  • 方法的名稱和描述符

常量池與運行時常量池:

  • 常量池:就是一張表(如上圖中的constant pool),虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量信息

  • 運行時常量池:常量池是*.class文件中的,當該類被加載以後,JVM才進行的動態鏈接,也就是説在運行期轉換後它的常量池信息就會放入運行時常量池,並把裏面的符號地址變為真實地址

#1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
#4 = Class              #21            // java/lang/Object
#7 = Utf8               <init>
#8 = Utf8               ()V
#18 = NameAndType        #7:#8          // "<init>":()V
#21 = Utf8               java/lang/Object

第一個常量是一個方法定義,指向了第4和第18個常量。以此類推查看第4和第18個常量。最後可以拼接成第一個常量右側的註釋內容:

java/lang/Object."<init>":()V

這段可以理解為該類的實例構造器的聲明,由於Main類沒有重寫構造方法,所以調用的是父類的構造方法。此處也説明了Main類的直接父類是Object。 該方法默認返回值是V, 也就是void,無返回值。

第二個常量同理可得:

#2 = Fieldref           #3.#19         // com/rhythm7/Main.m:I
#3 = Class              #20            // com/rhythm7/Main
#5 = Utf8               m
#6 = Utf8               I
#19 = NameAndType        #5:#6          // m:I
#20 = Utf8               com/rhythm7/Main

此處聲明瞭一個字段m,類型為I, I即是int類型。關於字節碼的類型對應如下:

對於數組類型,每一位使用一個前置的 [ 字符來描述,如定義一個java.lang.String[][] 類型的維數組,將被記錄為 [[Ljava/lang/Strin

方法表集合

在常量池之後的是對類內部的方法描述,在字節碼中以表的集合形式表現

private int m;
  descriptor: I
  flags: ACC_PRIVATE

此處聲明瞭一個私有變量m,類型為int,返回值為int

public com.rhythm7.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 3: 0
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       5     0  this   Lcom/rhythm7/Main;

這裏是構造方法:Main(),返回值為void, 公開方法。

code內的主要屬性為:

  • stack: 最大操作數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操作棧深度,此處為1

  • locals: 局部變量所需的存儲空間,單位為Slot, Slot是虛擬機為局部變量分配內存時所使用的最小單位,為4個字節大小。方法參數(包括實例方法中的隱藏參數this),顯示異常處理器的參數(try catch中的catch塊所定義的異常),方法體中定義的局部變量都需要使用局部變量表來存放。值得一提的是,locals的大小並不一定等於所有局部變量所佔的Slot之和,因為局部變量中的Slot是可以重用的。

  • args_size: 方法參數的個數,這裏是1,因為每個實例方法都會有一個隱藏參數this

  • attribute_info: 方法體內容,0,1,4為字節碼"行號",該段代碼的意思是將第一個引用類型本地變量推送至棧頂,然後執行該類型的實例方法,也就是常量池存放的第一個變量,也就是註釋裏的java/lang/Object.""😦)V, 然後執行返回語句,結束方法。LineNumberTable: 該屬性的作用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關係。可以使用 -g:none 或-g:lines選項來取消或要求生成這項信息,如果選擇不生成

  • LineNumberTable,當程序運行異常時將無法獲取到發生異常的源碼行號,也無法按照源碼的行數來調試程序。

  • LocalVariableTable: 該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關係。可以使用 -g:none 或 -g:vars來取消或生成這項信息,如果沒有生成這項信息,那麼當別人引用這個方法時,將無法獲取到參數名稱,取而代之的是arg0, arg1這樣的佔位符。

    • start 表示該局部變量在哪一行開始可見

    • length表示可見行數

    • Slot代表所在幀棧位置

    • Name是變量名稱

    • Signature 類型簽名

方法體內的內容是:將this入棧,獲取字段#2並置於棧頂, 將int類型的1入棧,將棧內頂部的兩個數值相加,返回一個int類型的值。

字節碼的好處,編譯型與解釋型

採用字節碼的好處?java程序通過編譯器編譯成字節碼文件,也就是計算機可以識別的二進制。java虛擬機就是將字節碼文件解釋成二進制段。採用字節碼的最大好處是:可以實現一次編譯到處運行,也就是java的與平台無關性

編譯型:編譯型語言open in new window 會通過編譯器open in new window將源代碼一次性翻譯成可被該平台執行的機器碼。一般情況下,編譯語言的執行速度比較快,開發效率比較低。常見的編譯性語言有 C、C++、Go、Rust 等等。

解釋型:解釋型語言open in new window會通過解釋器open in new window一句一句的將代碼解釋(interpret)為機器代碼後再執行。解釋型語言開發效率比較快,執行速度比較慢。常見的解釋性語言有 Python、JavaScript、PHP 等等。

Java 語言“編譯與解釋並存”?

這是因為 Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵。因為 Java 程序要經過先編譯,後解釋兩個步驟,由 Java 編寫的程序需要先經過編譯步驟,生成字節碼(.class 文件),這種字節碼必須由 Java 解釋器來解釋執行

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.