一、javap命令簡述
javap是jdk自帶的反解析工具。它的作用就是根據class字節碼文件,反解析出當前類對應的code區(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。
當然這些信息中,有些信息(如本地變量表、指令和代碼行偏移量映射表、常量池中方法的參數名稱等等)需要在使用javac編譯成class文件時,指定參數才能輸出,比如,你直接javac xx.java,就不會在生成對應的局部變量表等信息,如果你使用javac -g xx.java就可以生成所有相關信息了。如果你使用的eclipse,則默認情況下,eclipse在編譯時會幫你生成局部變量表、指令和代碼行偏移量映射表等信息的。
通過反編譯生成的匯編代碼,我們可以深入的了解java代碼的工作機制。比如我們可以查看i++;這行代碼實際運行時是先獲取變量i的值,然后將這個值加1,最后再將加1后的值賦值給變量i。
通過局部變量表,我們可以查看局部變量的作用域范圍、所在槽位等信息,甚至可以看到槽位復用等信息。
javap的用法格式:javap <options> <classes>
其中classes就是你要反編譯的class文件。
在命令行中直接輸入javap或javap -help可以看到javap的options有如下選項:
-help --help -? 輸出此用法消息 -version 版本信息,其實是當前javap所在jdk的版本信息,不是class在哪個jdk下生成的。 -v -verbose 輸出附加信息(包括行號、本地變量表,反匯編等詳細信息) -l 輸出行號和本地變量表 -public 僅顯示公共類和成員 -protected 顯示受保護的/公共類和成員 -package 顯示程序包/受保護的/公共類 和成員 (默認) -p -private 顯示所有類和成員 -c 對代碼進行反匯編 -s 輸出內部類型簽名 -sysinfo 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列) -constants 顯示靜態最終常量 -classpath <path> 指定查找用戶類文件的位置 -bootclasspath <path> 覆蓋引導類文件的位置
一般常用的是-v -l -c三個選項。
javap -v classxx,不僅會輸出行號、本地變量表信息、反編譯匯編代碼,還會輸出當前類用到的常量池等信息。
javap -l 會輸出行號和本地變量表信息。
javap -c 會對當前class字節碼進行反編譯生成匯編代碼。
查看匯編代碼時,需要知道里面的jvm指令,可以參考官方文檔:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
另外通過jclasslib工具也可以看到上面這些信息,而且是可視化的,效果更好一些。
二、javap測試及內容詳解
前面已經介紹過javap輸出的內容有哪些,東西比較多,這里主要介紹其中code區(匯編指令)、局部變量表和代碼行偏移映射三個部分。
如果需要分析更多的信息,可以使用javap -v進行查看。
另外,為了更方便理解,所有匯編指令不單拎出來講解,而是在反匯編代碼中以注釋的方式講解(吐槽一下,簡書的markdown貌似不能改字體顏色,這一點很不爽)。
下面寫段代碼測試一下:
例子1:分析一下下面的代碼反匯編之后結果:
public class TestDate { private int count = 0; public static void main(String[] args) { TestDate testDate = new TestDate(); testDate.test1(); } public void test1(){ Date date = new Date(); String name1 = "wangerbei"; test2(date,name1); System.out.println(date+name1); } public void test2(Date dateP,String name2){ dateP = null; name2 = "zhangsan"; } public void test3(){ count++; } public void test4(){ int a = 0; { int b = 0; b = a+1; } int c = a+1; } }
上面代碼通過JAVAC -g 生成class文件,然后通過javap命令對字節碼進行反匯編:$ javap -c -l TestDate
得到下面內容(指令等部分是我參照着官方文檔總結的):
Warning: Binary file TestDate contains com.justest.test.TestDate Compiled from "TestDate.java" public class com.justest.test.TestDate { //默認的構造方法,在構造方法執行時主要完成一些初始化操作,包括一些成員變量的初始化賦值等操作 public com.justest.test.TestDate(); Code: 0: aload_0 //從本地變量表中加載索引為0的變量的值,也即this的引用,壓入棧 1: invokespecial #10 //出棧,調用java/lang/Object."<init>":()V 初始化對象,就是this指定的對象的init()方法完成初始化 4: aload_0 // 4到6表示,調用this.count = 0,也即為count復制為0。這里this引用入棧 5: iconst_0 //將常量0,壓入到操作數棧 6: putfield //出棧前面壓入的兩個值(this引用,常量值0), 將0取出,並賦值給count 9: return //指令與代碼行數的偏移對應關系,每一行第一個數字對應代碼行數,第二個數字對應前面code中指令前面的數字 LineNumberTable: line 5: 0 line 7: 4 line 5: 9 //局部變量表,start+length表示這個變量在字節碼中的生命周期起始和結束的偏移位置(this生命周期從頭0到結尾10),slot就是這個變量在局部變量表中的槽位(槽位可復用),name就是變量名稱,Signatur局部變量類型描述 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/justest/test/TestDate; public static void main(java.lang.String[]); Code: // new指令,創建一個class com/justest/test/TestDate對象,new指令並不能完全創建一個對象,對象只有在初,只有在調用初始化方法完成后(也就是調用了invokespecial指令之后),對象才創建成功, 0: new //創建對象,並將對象引用壓入棧 3: dup //將操作數棧定的數據復制一份,並壓入棧,此時棧中有兩個引用值 4: invokespecial #20 //pop出棧引用值,調用其構造函數,完成對象的初始化 7: astore_1 //pop出棧引用值,將其(引用)賦值給局部變量表中的變量testDate 8: aload_1 //將testDate的引用值壓入棧,因為testDate.test1();調用了testDate,這里使用aload_1從局部變量表中獲得對應的變量testDate的值並壓入操作數棧 9: invokevirtual #21 // Method test1:()V 引用出棧,調用testDate的test1()方法 12: return //整個main方法結束返回 LineNumberTable: line 10: 0 line 11: 8 line 12: 12 //局部變量表,testDate只有在創建完成並賦值后,才開始聲明周期 LocalVariableTable: Start Length Slot Name Signature 0 13 0 args [Ljava/lang/String; 8 5 1 testDate Lcom/justest/test/TestDate; public void test1(); Code: 0: new #27 // 0到7創建Date對象,並賦值給date變量 3: dup 4: invokespecial #29 // Method java/util/Date."<init>":()V 7: astore_1 8: ldc #30 // String wangerbei,將常量“wangerbei”壓入棧 10: astore_2 //將棧中的“wangerbei”pop出,賦值給name1 11: aload_0 //11到14,對應test2(date,name1);默認前面加this. 12: aload_1 //從局部變量表中取出date變量 13: aload_2 //取出name1變量 14: invokevirtual #32 // Method test2: (Ljava/util/Date;Ljava/lang/String;)V 調用test2方法 // 17到38對應System.out.println(date+name1); 17: getstatic #36 // Field java/lang/System.out:Ljava/io/PrintStream; //20到35是jvm中的優化手段,多個字符串變量相加,不會兩兩創建一個字符串對象,而使用StringBuilder來創建一個對象 20: new #42 // class java/lang/StringBuilder 23: dup 24: invokespecial #44 // Method java/lang/StringBuilder."<init>":()V 27: aload_1 28: invokevirtual #45 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 31: aload_2 32: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 35: invokevirtual #52 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 38: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V invokevirtual指令表示基於類調用方法 41: return LineNumberTable: line 15: 0 line 16: 8 line 17: 11 line 18: 17 line 19: 41 LocalVariableTable: Start Length Slot Name Signature 0 42 0 this Lcom/justest/test/TestDate; 8 34 1 date Ljava/util/Date; 11 31 2 name1 Ljava/lang/String; public void test2