javap淺析-書籍第3章的手寫稿樣稿
3.2.1javap命令工具
第1章中我們就提到了有些地方需要用javap命令工具來看編譯後的指令是什麼,第2.2.1節中胖哥使用了一個簡單的程序讓大家感受了一下javap命令工具是什麼,這裏再次談到javap命令工具了。或許這一次我們可以對javap命令工具說得稍微清楚一點。為此,胖哥會單獨再寫幾段小程序給大家說說javap命令工具的結果怎麼看。
胖哥為什麼要給簡單程序呢?為啥不直接來個複雜的程序呢? 答曰:javap命令工具輸出的內容是繁雜的,即使是一段小程序輸出後,結果也比原始代碼要複雜很多。我們要學的其實並不是說看指令就能完全反轉為Java代碼,把自己當成一個“反編譯工具”(除非你真的已經很牛了,自然本書接下來的內容也不適合你),要學會的是通過這種方式可以認知比Java更低一個抽象層次的邏輯,或許有許多問題直接用Java代碼不好解釋,但是一旦看到虛指令後就一切明了。 在本節,胖哥分別演示String的小代碼,和幾段數字處理的小程序(延續下第1章的數字遊戲)。 |
String的代碼還少嗎?第1章就很多了?
沒錯,胖哥沒有必要再來寫第1章寫過的那些小程序,就用它們來做實驗吧。首先來回顧下代碼清單1-1的例子(這裏僅截圖),如下圖所示:
圖 3-1 代碼清單1-1的還原
當時我們提到這個結果是true,並且解釋了它是在編譯時被優化,現在就用javap指令來論證下這個結論吧:
D:\java_A>javac –g:vars,lines chapter01/StringTest.java D:\java_A>javap -verbose chapter01.StringTest public class chapter01.StringTest extends java.lang.Object minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#21; // java/lang/Object."<init>":()V const #2 = String #22; // ab1 const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V const #5 = class #27; // chapter01/StringTest const #6 = class #28; // java/lang/Object const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz LocalVariableTable; const #12 = Asciz this; const #13 = Asciz Lchapter01/StringTest;; const #14 = Asciz test1; const #15 = Asciz a; const #16 = Asciz Ljava/lang/String;; const #17 = Asciz b; const #18 = Asciz StackMapTable; const #19 = class #29; // java/lang/String const #20 = class #30; // java/io/PrintStream const #21 = NameAndType #7:#8;// "<init>":()V const #22 = Asciz ab1; const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V const #27 = Asciz chapter01/StringTest; const #28 = Asciz java/lang/Object; const #29 = Asciz java/lang/String; const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V;
{ public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0
LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest;
public static void test1(); Code: Stack=3, Locals=2, Args_size=0 0: ldc #2; //String ab1 2: astore_0 3: ldc #2; //String ab1 5: astore_1 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_0 10: aload_1 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 18 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] } |
好長好長的篇幅啊!
沒關係,我們慢慢來看哈!
首先我們看比較靠前的一個部分是:“常量池”(Constant pool),每一項都以“const #數字”開頭,這個數字是順序遞增的,通常把它叫做常量池的入口位置,當程序中需要使用到常量池的時候,就會在程序的對應位置記錄下入口位置的標識符(在字節碼文件中,就像一個列表一樣,列表中的每一項存放的內容和長度是不一樣的而已)。
根據入口位置肯定是要找某些常量內容,常量內容會分為很多種。在每個常量池項最前麵的1個字節,來標誌常量池的類型(我們看到的Method、String等等都是經過映射轉換後得到的,字節碼中本身隻會有1個字節來存放)。
找到類型後,接下來就是內容,內容可以是直接存放在這個常量池的入口中,也可能由其它的一個或多個常量池域組合而成,聽起來蠻抽象,胖哥來給大家講幾個例子:
例子1: const #1 = Method #6.#21; // java/lang/Object."<init>":()V 入口位置#1,簡稱入口#1,代表一個方法入口,方法入口由:入口#6 和 入口#21兩者一起組成,中間用了一個“.”。 const #6 = class #28; // java/lang/Object const #21 = NameAndType #7:#8;// "<init>":()V 入口#6為一個class,class是一種引用,所以它引用了入口#28的常量池。 入口#21 代表一個表示名稱和類型(NameAndType),分別由入口#7和入口#8組成。 const #7 = Asciz <init>; const #8 = Asciz ()V; const #28 = Asciz java/lang/Object; 入口#7是一個常量池內容,<init>;代表構造方法的意思。 入口#8 也是一個真正的常量,值為()V,代表沒有入口參數,返回值為void,將入口#7和入口#8反推到入口#21,就代表名稱為構造方法的名稱,入口參數個數為0,返回值為void的意思。 入口#28是一個常量,它的值是“java/lang/Object;”,但這隻是一個字符串值,反推到入口#6,要求這個字符串代表的是一個類,那麼自然代表的類是java.lang.Object。 綜合起來就是:java.lang.Object類的構造方法,入口參數個數為0,返回值為void,其實這在const #1後麵的備注中已經標識出來了(這在字節碼中本身不存在,隻是javap工具幫助合並的)。 例子2: const #2 = String #22; // ab1 它代表將會有一個String類型的引用入口,而引用的是入口#22的內容。 const #22 = Asciz ab1; 這裏代表常量池中會存放內容ab1。 綜合起來就是:一個String對象的常量,存放的值是ab1。 例子3(稍微複雜一點): const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V 入口#3代表一個屬性,這個屬性引用了入口#23的類,入口#24的具體屬性。 入口#4代表一個方法,引用了入口#25的類,入口#26的具體方法。 const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V 入口#23 代表一個類(class),它也是一個引用,它引用了入口#31的常量。 入口#24 代表一個名稱和類型(NameAndType),分別對應入口#32:#33。 入口 #25 代表一個class類的引用,具體引用到入口#30。 入口 #26 與入口#24類似,也是一個返回值+引用類型對應入口#34:#35。 const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V; 入口#30 對應常量池的值為:java/io/PrintStream;反推到入口#25,自然代表類java.lang.PrintStream。 入口#31對應常量池的值為:java/lang/System;反推到入口#23,代表類:java.lang.System。 入口#32 對應常量池的值為:out;反推到入口#24,而入口#24要求名稱和類型,這裏返回的顯然是名稱。 入口#33 對應常量池的值為:Ljava/io/PrintStream;; 反推到入口#24這裏得到了類型,也就是out的類型是java.io.PrintStream。 入口#34 對應常量池的值為:println;反推到入口#26代表名稱為println。 入口#35 對應常量池的值為:(Z)V;反推到入口#26代表入口參數為Z(代表boolean類型),返回值類型是V(代表void) 綜合來講要執行的操作就是: 入口#3是獲取到java/lang/System類的屬性out,out的類型是Ljava/io/PrintStream; 入口#4是調用java/io/PrintStream類的println方法,方法的返回值類型是void,入口類型是boolean。 |
小夥伴們應該發現到這個常量池僅僅是操作的陳列,還沒有真正的開始執行任務,那麼自然就要開始看第2部分的內容,它通過指令將這些內容組合起來。從輸出的結果來看,這些的指令是按照方法分開的(其實前麵應當還有屬性列表),首先看第一個方法:
public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0
LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; |
這是一個構造方法,程序中我們沒有寫構造方法,但是Java自己會幫我們生成一個,說明這個動作是在編譯時完成的。雖然是構造方法,但是它足夠簡單,所以我們先從它開始來說,請看胖哥的解釋:
Stack=1, Locals=1, Args_size=1 這一行是所有的方法都會有的,其中Stack代表棧頂的單位大小(每一個大小為一個solt的大小,每個solt是4個字節的寬度),當一個數據需要使用時首先會被放入到棧頂,使用完後會寫回到本地變量或主存中。這裏的棧的寬度是1,其實是代表有一個this將會被使用。 Locals是本地變量的slot個數,但是並不代表是stack寬度一致,本地變量是在這個方法生命周期內,局部變量最多的時候,需要多大的寬度來存放數據(double、long會占用兩個slot)。 Args_size代表的是入參的個數,不再是slot的個數,也就是傳入一個long,也隻會記錄1。 0: aload_0 首先第一個0代表虛指令中的行號(後麵會應到,確切說應該是方法的body部分第幾個字節),每個方法從0開始順序遞增,但是可以跳躍,跳躍的原因在於一些指令還會接操作的內容,這些操作的內容可能來自常量池,也可以標誌是第幾個slot的本地變量,因此需要占用一定的空間。 aload_0指令是將“第1個”slot所在的本地變量推到棧頂,並且這個本地變量是引用類型的,相關的指令有:aload_[0-3](範圍是:0x2a ~ 0x2d)。如果超過4個,則會使用“aload + 本地變量的slot位置”來完成(此時會多占用1個字節來存放),前者是通過具體的幾個指令直接完成。 許多地方會解釋為第1個引用類型的本地變量,但胖哥是一個邏輯怪,認為這句話有問題,並不是第1個引用變量,普通變量如果在它之前,它也不是第1個了,此時本身就是第1個本地變量,更確切地說是第一個slot所在位置的本地變量。 1: invokespecial #1; //Method java/lang/Object."<init>":()V 指令中的第2個行號,執行invokespecial指令,這個指令是當發生構造方法調用、父類的構造方法調用、非靜態的private方法調用會使用該指令,這裏需要從常量池中獲取一個方法,這個地方會占用2個字節的寬度,加上指令本身就是3個字節,因此下一個行號是4。 4: return 最後一行是一個return,我們雖然沒有自己寫return,但是JVM中會自動在編譯時加上。 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; 代表本地變量的列表,這裏代表本地變量的作用域起始位置為0,作用域寬度為5(0-4),slot的起始位置也是0,名稱為this,類型為chapter01.StringTest。 |
看了構造方法後,如果你理解了,再來看test1方法或許我們會輕鬆一點,不過大家可以在這個時候先養一養神,再來看哦。胖哥對於細節就不再一一講述,就在指令後麵寫備注即可:
public static void test1(); Code: Stack=3, Locals=2, Args_size=0 //Stack=3代表本地棧slot個數為3,兩個String需要load,System的out也會占用一個,當發生對比生成boolean的時候,會將兩個String的引用從棧頂pop出來,所以棧最多3個slot //Locals為2,因為隻有兩個String //如果是非靜態方法本地變量會自動增加this. //Args_size為0代表這個方法沒有任何入口參數 0: ldc #2; //String ab1 //指令body部分從第0個字節為Idc指令,從常量池入口#2中取出內容推到棧頂 //這裏的String也是引用,但是它是常量,所以是用Idc指令,不是aload指令 2: astore_0 //將棧頂的引用值,寫入第1個slot所在的本地變量中。 //它與aload指令正好相反,對應astore_[0-3](範圍是0x4b、0x4e) //更多的本地引用變量寫入則使用atore + 引用變量的slot位置。 3: ldc #2; //String ab1 //與第0行一致的操作,引用常量池入口#2來獲得 5: astore_1 //類似第2行,將棧頂的值賦值給第2個slot位置的本地引用變量。 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; //獲取靜態域,放入棧頂,引用了常量池入口#3來獲得 //此時的靜態區域是System類中的out對象 9: aload_0 //將第1個slot所在位置的本地引用變量加載到棧頂 10: aload_1 //將第二個slot所在位置的本地引用變量加載到棧頂 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 //判定兩個棧頂的引用是否一致(引用值也就是地址),對比處理的結束位置是18行 // if_acmpne操作之前會先將兩個操作數從棧頂pop出來,因此棧頂最多3位 //如果一致則將常量值1寫入到棧頂,也就是對應到boolean值true,並跳轉到19行 //如果不一致則將常量值0寫入到棧頂,對應到boolean值false 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V //執行out對象的println方法,方法的入口參數是boolean類型,返回值是void。 //從常量池入口#4獲得方法的內容實體。 //此時會將棧頂的元素當成入口參數,棧頂的0或1則會轉換為boolean值的true、false。 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 //對應源文件行號,左邊的是字節碼的位置(也可以叫做行號),右邊的是源文件中的實際文本行號 //javac編譯默認有這個內容,但是如果-g:none則不會產生,那麼調試就會有問題 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; //本地變量列表,javac中需要使用-g:vars才會生成,使用一些工具會自動生成,若沒有,則調試的時候,斷點中看到的變量是沒有名稱的。 //第一個本地變量的作用區域從第3個字節的位置開始,作用區域範圍為20個字節,所在slot的位置是第0個位置,名稱為a,類型為java.lang.String。 //第二個本地變量也是類似的方式可以得到結果。 |
在這裏,還有一些內容並沒有細化,例如StackMapTable的內容,這些請在研究清楚現有的內容後,就可以自己繼續去深入和細化了,因為這部分內容會包含的知識是非常多的,關於指令部分,大家可以參考官方文檔的介紹來學習。
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-7.html
我們回過頭來看問題,為何會輸出true就很簡單了,第一個變量a,代碼中本身編寫的是”a” + “b” + 1的操作,但是在常量池中卻找不到這3個值,而且指令中也看不到對它們的操作,指令中隻看到了對字符串”ab1”的操作,因此在編譯階段,JVM就將它合並了,這樣我們不用去聽別人說怎麼優化,看看便知道。
這樣貌似就是去找一些鑽牛角尖的問題?
其實不然,其實是幫我們從根本上去了解一些細節,或者說是相對抽象層次較低的細節,當然可能你平時用不上,當我們真的有一天遇到一些詭異的問題,就可能用得上了。為此,胖哥再來個例子。
神馬例子呢?很好奇哦!
第1章我們玩了點數字遊戲,也許大家沒有玩爽,當時胖哥說這一章會有,這一次我們來看那看一個簡單的數字操作的指令細節是什麼。在這之前,我們先看看代碼如下所示(代碼是放在第1章內的,但是說明問題是在本章開始說明):
public static void test() { int a = 1 , b = 1 , c = 1 , d = 1;
a++; ++b;
d = ++d;
System.out.println(a + "\t" + b + "\t" + c + "\t" + d); } |
執行結果你猜到了嗎,還是你確認了結果!
我們一起來看看輸出結果吧:
2 2 1 2 |
胖哥此時估計有的小夥伴也驚呆了,為何會有一個1呢?
其餘的幾個結果為2的答案很好解釋,但是這個1這個答案怎麼解釋呢?
教科書上通常告訴我們:i++是先做操作再自增,而++i是先自增再做操作。好的,我們按照這種思路來理解下c = c++;這條代碼,如果是先做操作,那麼這裏隻有賦值操作,就是c賦值給c,再自增顯然自增後應該是2,但是輸出的結果1,解釋不通。難道是先自增在賦值?如果是這樣的話,結果也應該是2才對。
小夥伴們迷茫了。這TNND的到底是怎麼回事呢?有的小夥伴可能會說這是多麼鑽牛角尖的問題啊。胖哥也是這麼認為的,這樣的問題或許結果並不重要,重要的是它可以讓我們了解到一個簡單的自增操作不止一個步驟來完成的,讓我們真正擁有一種去探索知識內在的興趣。
教科書上的說法僅僅是為了方便大家理解而給出的一種通用說法,每一種語言在實現它的時候,都有自己的實現方式,我們是Java程序員,自然需要知道Java程序是怎麼處理它的了(否則我們就真的就不專業了哦)。
難道自己思考是怎麼回事嗎,其實這種思考就是猜測了哦,猜測下可以鍛煉下猜測能力,不過最終還得了解本質,看看這一小節告訴我們的指令就知道啦,就用它來輸出指令看看指令裏麵到底做了什麼(篇幅所限,這裏不再看常量池,隻說指令,而且隻說關鍵部分)。
public static void test(); Code: Stack=3, Locals=4, Args_size=0 0: iconst_1 //將int類型常量值1推送到棧頂 1: istore_0 //將棧頂拋出賦值給第1個slot所在的int類型本的變量中 2: iconst_1 //與第0行一致 3: istore_1 //將棧頂拋出賦值給第2個slot所在的int類型本的變量中 4: iconst_1 //與第0行一致 5: istore_2 //將棧頂拋出賦值給第3個slot所在的int類型本的變量中 6: iconst_1 //與第0行一致 7: istore_3 //將棧頂拋出賦值給第4個slot所在的int類型本的變量中 8: iinc 0, 1 //將第1個slot所在的int類型本的變量自加1 11: iinc 1, 1 //將第2個slot所在的int類型本的變量自加1 14: iload_2 //將第3個slot所在的int類型本的變量放入棧頂 15: iinc 2, 1 //將第3個slot所在的int類型本的變量加1 18: istore_2 //從棧頂拋出數據寫入到第3個slot所在的int類型本的變量 19: iinc 3, 1 //將第4個slot位置所在的int類型的本變量自增1 22: iload_3 //將第4個slot位置所在的int類型的本地變量加載到棧頂 23: istore_3 //將棧頂數據拋出,寫入到第4個slot所在的int類型的本地變量中 LocalVariableTable: Start Length Slot Name Signature 2 70 0 a I //本地變量a,類型int,作用域第2行開始,作用域範圍70行 4 68 1 b I//本地變量b,類型int,作用域第4行開始,作用域範圍68行 6 66 2 c I//本地變量c,類型int,作用域第6行開始,作用域範圍66行 8 64 3 d I//本地變量d,類型int,作用域第8行開始,作用域範圍64行 |
現在我們來逐步看問題,首先發現的第一個特征是第8行、第11行,它們都做了iinc操作,都是對本地變量做疊加操作,分別是對前麵兩個本地變量(a、b)做疊加操作,後續沒有其它的動作。換句話說,當一個本地變量發生i++或++i的操作的時候,如果這個代碼發生在單行上麵,即不會用於其它的計算操作,它們最終的指令都是iinc,也就是i++也會被改為++i操作。
進一步來看第3個本地變量c的操作,首先是通過iload_2指令將其拷貝到棧頂,然後發生iinc操作(即自增操作),然後通過istore_2指令將棧頂的數據賦值給這個本地變量,因此,你可以認為它就像做了一個這樣的操作:
int tmp = c; c++; c= tmp;
這樣3個步驟的動作,隻是這個tmp並不是真實存在的本地變量,而是棧頂的一份數據拷貝,這一份拷貝的數據其實是為其它的操作,而自己疊加數據並不參與其它的計算,這才是Java中實現i++的真實道理。
對比d的操作,可以看到d是先進行了iinc操作,然後再做iload、istore的兩個動作用於賦值的,所以d是會被疊加的,隻是最後兩個動作是多餘的而已。
這樣小夥伴們是不是有點暈了!
我們畫個圖來看看,或許你會清楚一點。
首先來看看,進入方法前,JVM分配的棧大概是什麼樣子的(這個部分不包含指令及指令中指向的常量池位置):

圖 3-2 初始化一個方法後,大概是這樣的哦
當iconst_1發生的時候,結構就發生改變了:

當istore_0發生操作的時候,將棧頂拋出,賦值給變量a,此時的結構變成這樣:

以此類推,發生到第7行,對4個本地變量都會發生這樣的賦值,結果為:
圖 3-5 分別通過棧定賦值後的結果,棧頂隻用了一個slot
iinc指令我們沒有必要講解(實現的細節也可以是利用了一個棧頂來store、疊加1、load),總之a、b兩個變量變成了2。當再進一步做c = c++操作的時候首先發生第1個步驟是將數據拷貝到棧頂,然後將本地變量改為2,然後再從棧頂拷貝回來,如下圖所示:

圖 3-6 c=c++操作的程序運行過程
小夥伴們看懂了,但是又有的小夥伴著急了:後進先出棧明明隻用一個slot為什麼會有3個呢?
能問出這個問題說明你懂得思考,其實剛開始我們隻是輸出了一些簡單的操作指令,後來還有一條代碼System.out.println(a+ "\t" + b + "\t" + c + "\t" + d);相關指令還沒有輸出呢。別看這就一行代碼,指令可多了哦(寫代碼寫得短,並不代表指令短,也就是不能代表跑得快),一起來看看接下來的一些指令:
24: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 27: new #3; //class java/lang/StringBuilder 30: dup 31: invokespecial #4; //Method java/lang/StringBuilder."<init>":()V 34: iload_0 35: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 38: ldc #6; //String \t 40: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 43: iload_1 44: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 47: ldc #6; //String \t 49: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 52: iload_2 53: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 56: ldc #6; //String \t 58: invokevirtual #7;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 61: iload_3 62: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 65: invokevirtual #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 68: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 71: return |
好長,其實大部分指令都是invokevirtual指令,關鍵它在操作什麼。其實大夥看看後麵的注釋應該就看懂了(這也是剛開始看不懂虛指令,可以直接看注釋的方式)。現在我們想要知道本地棧要使用3個slot怎麼來的。
首先getstatic指令要將System類的out這個靜態屬性獲取出來放入棧頂(因為沒有局部變量存放,隻能放在棧頂),接著通過new指令創建一個對象,這個對象通過常量池入口#3獲得是一個StringBuilder類型(這是證明第1章中提到的字符串拚接的結論)。此時棧的樣子應當是這樣的:
圖 3-7 執行前兩條指令後,棧空間的情況
此時發生的是dup命令,它會拷貝一份棧頂的內容,並寫入棧頂,為什麼要這樣做呢?因為後續的invokespecial操作將會棧頂的信息拋出執行,執行StringBuilder的構造方法(小夥伴們又著急了,不是已經創建了嗎,幹嘛還有構造方法,其實剛才的new僅僅分配了空間,還沒有對內容進行初始化呢,一個簡單的創建對象其實需要多條指令來完成的)。因此此時的棧就變成了這樣的情況:

圖3-8 執行dup指令過後的情況
這裏3個棧就用過了,小夥伴們應該清楚了Stack為3的情況了吧!大家可以將這條代碼去掉,或自己用一個StringBuilder來拚接看看結果是什麼樣子的。
接下來就將棧頂拋出,執行StringBuilder的構造方法(兩個引用其實引用同一個對象),初始化後就變得和圖3-7一樣,隻是現在的StringBuilder已經執行完構造方法(但是並不代表所有屬性都初始化完成,在第5章會提到重排序的問題)。
緊接著,將本地變量、常量“\t”逐個iload或aload到棧頂,然後調用invokevirtual指令調用StringBuilder類的append方法,雖然它也會pop出來做操作,但這個方法會有一個StringBuilder返回值,由於下一個動作是基於這個返回值來操作,所以這個返回值將會再次被賦值到棧頂,因此它執行前無需再拷貝了,如果這個StringBuilder是一個自定義的本地變量,也無需再一次iload操作。
大家可以在這段代碼上做幾個小改動,進一步分析: ○ 將拚接過程換成一個StringBuilder,看看Stacks的數量有沒有變化。 ○ 換成一個StringBuilder在一行代碼中append多個變量,與分成多行分別append,指令上是否有區別(這裏append的內容有7個,你完全可以拆分2、3個出來看看)。 ○ 添加一個自定義對象,自定義對象中有一個void返回值的方法,也像append那樣反複調用,看看它是不是需要每次iload,而StringBuilder不需要。其實為什麼我們已經解釋過了,接下來就靠大家自己去擴展了哦。 |
胖哥隻是舉例說明一些簡單的例子,大家可以繼續擴展,例如(i++) + (++i) + (i++)等等,或許你看看指令就清楚了內在的執行順序。關於JVM的指令有200多個,我們要一一看完不容易,可以先看自己想看的一些指令,或者自己寫幾個簡單程序看看指令。等到我們知道了許多的指令後,再係統化的看這些指令,就很輕鬆了哦。
這些指令還是javap命令告訴我們的,javap命令本身也將字節碼翻譯成了文字,它比起反編譯工具隻是更加接近於字節碼的結構(大家也大概了解到反編譯工具就是基於這種指令反向計算出程序代碼的),但是它還不是真正的字節碼,如果有興趣的小夥伴們,可以看看下一節胖哥對於字節碼本身的介紹,然後javap命令工具是如何解析這個字節碼得到內容的。
這是本書的一個小樣章,內容格式貼進來全部亂了,請大家諒解。
哈哈,最後還是插播一個廣告:
大家覺得小胖的文章寫得還行的話,就投票吧,哈哈!
投票地址:https://vote.blog.csdn.net/blogstaritem/blogstar2013/xieyuooo
嗬嗬,覺得想吐槽就吐吧!最後更新:2017-04-03 12:53:49
上一篇:
Solr集群架構概述及delta-import詳細配置
下一篇:
HI3531的nand flash測試
為什麼阿裏巴巴的企業Logo是它?
01.部署NopCommerce時遇到的問題
Oracle E-Delivery網站發布了ALBPM 6.0.4最新版本,而不是通過BEA的網站進行發布
ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8
幣兌幣數字貨幣交易所開發,數字貨幣交易網站製作
React全家桶與前端單元測試藝術
黑客組織利用El Machete竊取全球政府超過100G數據
天氣預報進展
UVA之11300 - Spreading the Wealth
黑客是如何知道我們常用的密碼的