1. 程式人生 > >java位元組碼理解——Java bytecode:翻譯和解讀

java位元組碼理解——Java bytecode:翻譯和解讀

本篇部落格是對Java bytecode:這篇文章的翻譯和解讀,原文連結在這

http://www.ibm.com/developerworks/library/it-haggar_bytecode/index.html

如有不正之處還請各位指教,不喜勿噴,相互交流才能進步。

下面正片開始

生成java位元組碼:

javac Employee.java

javap -c Employee > Employee.bc

Generating bytecode

先將java原始碼進行編譯後再用javap命令進行反編譯並新增-c引數來獲得類的位元組碼。獲得位元組碼如下:

根據原文解讀可大致猜測,Employee.java

原始碼應該是這樣的:

由此可以發現:前五行的位元組碼是用哪個類生成的,這個類的定義,這個類是從哪個類繼承的,這個類的構造方法和其他的方法。下一步位元組碼將這個類的構造方法羅列出來,之後又將這個類的所有方法和與之相關的位元組碼用字典序羅列出來(這裡我嘗試了一下發現位元組碼中方法並沒有按照字典序排序..不知是否是我的理解錯誤,測試圖如下)。

拋開上面無關緊要的部分,繼續。。。

這時候你可能會發現,特定的操作碼的字首a和i

這個操作碼的字首’a’表示的意思為:說明正在操作的是一個物件的引用。

同理’i’表示的意思為:說明正在操作的是一個整型變數

除了’a’和’i’還有以下幾種操作碼字首:

‘b’:說明正在操作的是byte型別的變數

‘c’:說明正在操作的是char型別的變數

‘d’:說明正在操作的是double型別的變數

等等。。。

這裡要注意:獨立的程式碼一般會被jvm解析為操作碼,多重的操作碼指令一般會被解析為位元組碼。

The details

這裡為了更深入的理解jvm是如何執行位元組碼的,我們要理解一下jvm。

Jvm是基於堆疊的,對於jvm來說每一個執行緒都會有一塊獨立的堆疊,堆疊中儲存著棧幀,當執行緒中有方法被呼叫的時候就會建立一塊棧幀並將其壓入棧中,棧幀又由如下幾塊組成:運算元棧,區域性變數區和一塊當前執行緒所擁有的的類的引用(類的引用儲存在常量池中)。

對於區域性變數區來說,它既儲存方法的引數,也儲存方法中產生的一些中間變數。對於普通方法(靜態方法)而言區域性變數區首先儲存的是方法的引數(從0開始),接著再儲存方法中產生的區域性變數。但是對於構造方法或者是例項方法而言,首先儲存的是物件的引用(從0開始),接著第一個引數存1號位置,第二個引數存第二個位置,以此類推。對於靜態方法來說,由於靜態方法沒有是類所擁有的,與物件無關,因此它的第一個引數存0號位置,第二個引數存1號位置,以此類推。。。

區域性變數區的大小在編譯的時候已經決定了,它主要取決於引數和中間變數的個數和大小

運算元棧用棧來push和pop值,一些特定的指令集會將運算元壓入棧中,其他的指令會將這些運算元取走,並將執行的結果再次壓入棧中。

上面一段是java原始碼,下面一段是相應的位元組碼。

這一段位元組碼由3個指令集組成。

先看第一個指令集 aload_0 首先根據之前講的,’a’開頭說明操作的是一個引用,結尾的0表示的是從區域性變數區中取出放在0號位置的那個變數放入運算元棧,那麼為什麼要用aload而不用iload呢?我們看一下這個方法是一個例項方法,例項方法的區域性變數區的0號位置儲存的是物件引用(this),因此要用’a’開頭。所有的一切是那麼解釋通了。還有一個問題,jvm為什麼要取this引用呢?因為我們要用this引用來傳遞例項資料,名字等資訊。

再看第二個指令集getfield #5,這個指令用來獲取變數從一個物件中,當這個指令執行的時候,處於運算元棧頂端的this引用被拿出,和後面的#5組成一個索引去常量池中尋找相應屬性(這裡是找name,因為要獲取name屬性的引用)的引用,當這個引用找到並抓取了之後,將結果放入運算元棧中。

最後一個指令集areturn ,從這個方法中返回對應返回值的引用,並將這個返回值的引用從運算元棧中取出,壓入呼叫方法的運算元棧中(棧幀中)。

下面講一下最左邊的數字是怎麼來的,先上兩張圖:

相信你一定能夠看懂(我不會說是因為我懶而不想翻譯那麼大一段英文。。。這裡其實體現了java的平臺無關性)

接著我們看一下建構函式的位元組碼:

首先第一行位元組碼跟我們上面分析一樣,將構造方法的this引用取出來壓入運算元棧中。

第二行位元組碼是呼叫父類的構造方法,因為所有的沒有顯示繼承的類都隱式的繼承的Object類,因此這裡是呼叫了Object類的構造方法。也就是說,該構造方法的java原始碼其實應該是這樣的:

跟之前分析的一樣,當這行位元組碼執行完後,this引用從運算元棧中移除。

接下去的兩行位元組碼aload_0和aload_1,就是將this引用和構造方法的第一個引數取出(不明白為什麼的小夥伴請再閱讀一遍區域性變數區的那裡哈),放入運算元棧中。

接下去的putfield #5這行位元組碼就是將上一步壓入運算元棧中的this引用和strName值取出,並通過this和#5找到相應的strName引用將這個strName值賦給它。

下面那個給idNumber賦值的同理。

最後來看看最後5步aload_0 aload_1 iload_2 invokespecial #6 <Methodvoid storeData(java.lang.String, int)> return

前三條指令分別將this引用,strName值,idNumber值分別取出並壓入運算元棧,注意this引用必須要被壓入,因為這個例項方法正在被呼叫。如果這個方法是靜態方法的話,this引用就不必被壓入棧中,但是strName和idNumber必須要被壓入到運算元棧中,因為他們是storedData方法的引數。因此當storedData方法執行的時候,this引用,strName和idNumebr分別佔據storedData方法的區域性變數區的0,1,2個索引。

Size and speed issues

這個模組就是比較兩種相同功能的程式碼用怎麼寫比較快,解析成的位元組碼比較數量比較少。

下面看一下這兩種執行緒同步方式解析所產生的的位元組碼數量的區別:

原文說道第一種方式大概要比第二種方式要快13%左右。但是我認為這僅限於方法內部都需要同步的情況,當方法只需要部分同步的時候,這時候在高併發情況下,直接加鎖的方式效率顯然要低於synchrnized塊的方式。