javap -verbose輸出結果詳解

javap 是 jdk 自帶的一個工具,可以反編譯 class 文件,是我們在做 java 代碼性能分析時必不可少的一個工具。

我們先寫個簡單的代碼,然後我們在逐個分析 javap 解析出來的內容。

public class TestJavap {

    public static int add(int a, int b) {
        int r = a + b;
        return r;
    }

    public static void main(String[] args) {
        int r = add(15, 16);
        System.out.println(r);
    }

}

執行 javap -v TestJavap 之後獲得的內容如下:

D:\workspace\test_java\bin>javap -v TestJavap.class
Classfile /D:/workspace/test_java/bin/TestJavap.class
  Last modified 2013-12-31; size 643 bytes
  MD5 checksum 03f49f751716ceb852c190bfb54cbb2f
  Compiled from "TestJavap.java"
public class TestJavap
  SourceFile: "TestJavap.java"
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             //  TestJavap
   #2 = Utf8               TestJavap
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          //  java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          //  "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LTestJavap;
  #14 = Utf8               add
  #15 = Utf8               (II)I
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               b
  #19 = Utf8               r
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Methodref          #1.#23         //  TestJavap.add:(II)I
  #23 = NameAndType        #14:#15        //  add:(II)I
  #24 = Fieldref           #25.#27        //  java/lang/System.out:Ljava/io/PrintStream;
  #25 = Class              #26            //  java/lang/System
  #26 = Utf8               java/lang/System
  #27 = NameAndType        #28:#29        //  out:Ljava/io/PrintStream;
  #28 = Utf8               out
  #29 = Utf8               Ljava/io/PrintStream;
  #30 = Methodref          #31.#33        //  java/io/PrintStream.println:(I)V
  #31 = Class              #32            //  java/io/PrintStream
  #32 = Utf8               java/io/PrintStream
  #33 = NameAndType        #34:#35        //  println:(I)V
  #34 = Utf8               println
  #35 = Utf8               (I)V
  #36 = Utf8               args
  #37 = Utf8               [Ljava/lang/String;
  #38 = Utf8               SourceFile
  #39 = Utf8               TestJavap.java
{
  public TestJavap();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTestJavap;

  public static int add(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0     a   I
            0       6     1     b   I
            4       2     2     r   I

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: bipush        15
         2: bipush        16
         4: invokestatic  #22                 // Method add:(II)I
         7: istore_1
         8: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_1
        12: invokevirtual #30                 // Method java/io/PrintStream.println:(I)V
        15: return
      LineNumberTable:
        line 9: 0
        line 10: 8
        line 11: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            8       8     1     r   I
}

很長很恐怖,是吧。。。(如果是一個實際項目的class文件,那會恐怖得令人髮指),別急,讓我們來一點一點地分析:

Classfile /D:/workspace/test_java/bin/TestJavap.class
  Last modified 2013-12-31; size 643 bytes
  MD5 checksum 03f49f751716ceb852c190bfb54cbb2f
  Compiled from "TestJavap.java"
public class TestJavap
  SourceFile: "TestJavap.java"
  minor version: 0
  major version: 50

這部分不用多說,大家一看就明白。主要就是記錄一些基礎的版本信息。minor version: 0 major version: 50 指的是這個class文件編譯時所使用的 jdk 版本號。

常量池:

Constant pool:
   #1 = Class              #2             //  TestJavap
   #2 = Utf8               TestJavap
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          //  java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          //  "<init>":()V
   .......

Constant Pool (常量池),在java虛擬機中是個重要的概念。我們可以這樣理解一下,這個“池子”記錄了java程序運行所需要的所有符號,包括變量名、方法名、類名、字符串等一切符號。在下面的介紹中你會看到,在java方法執行時會經常引用常量池中的內容。#1,#2 這樣的數字可以理解爲常量池中的每一項的“索引地址”,字節碼指令會經常使用這個索引來引用對應的符號。

這裏張圖可以加深對常量池的理解:

(圖1:java 虛擬機的數據結構)

 

 (圖2:java class 文件結構)

 

 

是不是覺得jvm運行時離不開常量池

更多關於常量池的介紹可以參考:《Javaclassfile-The constant pool》, 以及《深入java虛機》一書

下面是重點,我們會詳細介紹方法字節碼錶示的含義。

比如方法 add 對應的java代碼和字節碼錶示爲:

public static int add(int a, int b) {
    int r = a + b;
    return r;
}
 ......
  public static int add(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0     a   I
            0       6     1     b   I
            4       2     2     r   I
   ......

其中 flags: ACC_PUBLIC, ACC_STATIC 這一行我覺得不用細講,一看就明白,這是類或方法的訪問標識,用來定義他們的訪問權限的。還有 ACC_FINAL ACC_ABSTRACT 等。他們和 public 、static、final 、abstract 這些關鍵字是對應的。

局部變量表:

下面我們先來介紹 LocalVariableTable(局部變量表)

我們要先有記住一點,jvm是基於棧的運算,先看一下上面的圖1(Java虛擬機運行時的數據結構)。每個java線程在運行時,jvm都會爲其分配一個“棧空間”(就是一個內存區域),主要包括一個PC寄存器(記錄當前線程運行的下一條指令),JVM棧空間,本地棧空間(本地代碼,一般是C寫的lib可以理解爲JNI的方式調用的代碼,和我們自己寫的java代碼無關了)。當某個java方法運行時,jvm會創建一個“棧幀”(也是一段內存空間),我們要介紹的LocalVariableTable就是“棧幀”的一部分,另外“棧幀”還包括我們常聽說的“操作數棧(Operand Stack)”和對常量池的引用(Reference To Constant Pool)。局部變量表中記錄了一個java方法運行時鎖需要的局部變量名(Name 這一列), Signature 是類型描述符,I就表示int類型(更多類型描述符參見:《Chapter 4. The class File Format》

這個還要介紹一個“Slot”的概念,一個 Slot 就可以理解爲一個 32 位(4字節)的內存單位。在我們的例子中,參數 a、b 臨時變量 r 都是 int 類型,在 java 中,int 類型就是一個4字節長度,即1個slot。在我們的例子中,LocalVariableTable 中有三個變量,都是 int 類型,需要 3 個 slot,所以看到 locals =3 這一行 就應該明白是什麼意思了吧。

我們在深入一點,把 a,b 和r 都換成 Long 類型,在 javap -v 一下,看看會變成什麼樣子:

代碼:

public static long add(long a, long b) {
    long r = a + b;
    return r;
}

對應的字節碼爲:

.......
  public static long add(long, long);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=6, args_size=2
         0: lload_0
         1: lload_2
         2: ladd
         3: lstore        4
         5: lload         4
         7: lreturn
      LineNumberTable:
        line 4: 0
        line 5: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0     a   J
            0       8     2     b   J
            5       3     4     r   J
   .......

是不是能找到點感覺啦? 因爲在 java 中 long 類型是 64 位,8 字節,要佔用 2 個 slot,所以 3 個變量共佔用 6 個 slot,所以這裏 locals = 6。Slot 這裏一列也不一樣了,是吧,說明,Slot 這一列可以看作變量空間的入口索引位置(Signature 下的 J 是 long 類型的類型描述符)。

棧寬:

stack 指的是棧的寬度——就是執行這個方法時,爲這個方法的操作數棧定義多少個slot,注意,這個寬度足以容納當前方法所有運算所需要的操作數,下面我們舉例說明。 上面的例子中,只有一個 a + b 的操作,每個參數都是 long 型(即 2 個slot), 執行這個加法運算的過程是這樣的,lload_0 指令把 LocalVariableTable 中索引爲 0 的操作數(變量a)壓入操作數棧中,lload_2 把索引爲 2 的操作數也壓入棧中,注意,這裏操作數棧中已經壓入了兩個 long 類型,共 4 個 slot,然後 ladd 指令從棧中彈出這兩個操作數(此時操作數棧空了),運算結束後在把運算結構再次壓入棧中,此時操作數棧中只有一個long類型的數據(佔用 2 個 slot),然後 lstore 把棧中的結果保存在局部變量表中索引爲 4 的位置(即變量r)。在這個過程中,“最多”佔用 4 個 slot(就是把 a 和 b 都壓入棧中的時候),所以 stack=4

字節碼偏移位置:

Code 代碼前的標號是字節碼指令的偏移(java的字節碼文件組織得是很緊湊的,每個字節都有其具體的含義)。 jvm 中每個字節碼佔用 1 個字節,上面了例子中,lload_0 、lload_2、ladd 3 個指令由於沒有操作數,所以它們幾個的偏移量分別爲0,1,2。第四個指令 lstore 後面跟了一個操作數索引參數(1個字節),其佔用2個字節,所以下一個質量的偏移量是從 5開始,一次類推。更多java字節碼質量參見:Java bytecode instruction listings

LineNumberTable

LineNumberTable 記錄字節碼行號和源代碼行號的對應關係。 比如 line 4: 0,左邊的4代表源碼的行號,後邊的0代表字節碼的起始偏移地址。這個信息是用來調試用了,我們經常看到的java拋出的異常時鎖所攜帶的線程調用棧的信息,就是跟這個表有關係。

 

出處https://www.coderxing.com/javap-verbose.html

 

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