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