天天看點

Java位元組碼淺析(二)

英文原文連結,譯文連結,原文作者:james bloom,譯者:有孚

像if-else, switch這樣的流程控制的條件語句,是通過用一條指令來進行兩個值的比較,然後根據結果跳轉到另一條位元組碼來實作的。

循環語句包括for循環,while循環,它們的實作方式也很類似,但有一點不同,它們通常都會包含一條goto指令,以便位元組碼實作循環執行。do-while循環不需要goto指令,因為它的條件分支是在位元組碼的末尾。更多細節請參考循環語句一節。

有一些指令可以用來比較兩個整型或者兩個引用,然後執行某個分支,這些操作都能在單條指令裡面完成。而像double,float,long這些值需要兩條指令。首先得去比較兩個值,然後根據結果,會把1,0或者-1壓到棧裡。最後根據棧頂的值是大于,等于或者小于0來判斷應該跳轉到哪個分支。

我們先來介紹下if-else語句,然後再詳細介紹下分支跳轉用到的幾種不同的指令。

下面的這個簡單的例子是用來比較兩個整數的:

<code>1</code>

<code>public</code> <code>int</code> <code>greaterthen(</code><code>int</code> <code>intone,</code><code>int</code> <code>inttwo) {</code>

<code>2</code>

<code>    </code><code>if</code> <code>(intone &gt; inttwo) {</code>

<code>3</code>

<code>        </code><code>return</code> <code>0</code><code>;</code>

<code>4</code>

<code>    </code><code>}</code><code>else</code> <code>{</code>

<code>5</code>

<code>        </code><code>return</code> <code>1</code><code>;</code>

<code>6</code>

<code>    </code><code>}</code>

<code>7</code>

<code>}</code>

方法最後會編譯成如下的位元組碼:

<code>0</code><code>: iload_1</code>

<code>1</code><code>: iload_2</code>

<code>2</code><code>: if_icmple    </code><code>7</code>

<code>5</code><code>: iconst_0</code>

<code>6</code><code>: ireturn</code>

<code>7</code><code>: iconst_1</code>

<code>8</code><code>: ireturn</code>

首先,通過iload_1, iload_2兩條指令将兩個入參壓入操作數棧中。if_icmple會比較棧頂的兩個值的大小。如果intone小于或者等于inttwo的話,會跳轉到第7行處的位元組碼來執行。可以看到這裡和java代碼裡的if語句的條件判斷正好相反,這是因為在位元組碼裡面,判斷條件為真的話會跑到else分支裡面去執行,而在java代碼裡,判斷為真會進入if塊裡面執行。換言之,if_icmple判斷的是如果if條件不為真,然後跳過if塊。if代碼塊裡對應的代碼是5,6處的位元組碼,而else塊對應的是7,8處的。

Java位元組碼淺析(二)

下面的代碼則稍微複雜了一點,它需要進行兩次比較。

<code>public</code> <code>int</code> <code>greaterthen(</code><code>float</code> <code>floatone,</code><code>float</code> <code>floattwo) {</code>

<code>    </code><code>int</code> <code>result;</code>

<code>    </code><code>if</code> <code>(floatone &gt; floattwo) {</code>

<code>        </code><code>result =</code><code>1</code><code>;</code>

<code>        </code><code>result =</code><code>2</code><code>;</code>

<code>8</code>

<code>    </code><code>return</code> <code>result;</code>

<code>9</code>

編譯後會是這樣:

<code>01</code>

<code>0</code><code>: fload_1</code>

<code>02</code>

<code> </code><code>1</code><code>: fload_2</code>

<code>03</code>

<code> </code><code>2</code><code>: fcmpl</code>

<code>04</code>

<code> </code><code>3</code><code>: ifle         </code><code>11</code>

<code>05</code>

<code> </code><code>6</code><code>: iconst_1</code>

<code>06</code>

<code> </code><code>7</code><code>: istore_3</code>

<code>07</code>

<code> </code><code>8</code><code>:</code><code>goto</code>          <code>13</code>

<code>08</code>

<code>11</code><code>: iconst_2</code>

<code>09</code>

<code>12</code><code>: istore_3</code>

<code>10</code>

<code>13</code><code>: iload_3</code>

<code>11</code>

<code>14</code><code>: ireturn</code>

在這個例子中,首先兩個參數會被fload_1和fload_2指令壓入棧中。和上面那個例子不同的是,這裡需要比較兩回。fcmple先用來比較棧頂的floatone和floattwo,然後把比較的結果壓入操作數棧中。

<code>* floatone &gt; floattwo –&gt;</code><code>1</code>

<code>* floatone = floattwo –&gt;</code><code>0</code>

<code>* floatone &lt; floattwo –&gt; -</code><code>1</code>

<code>* floatone or floattwo = nan –&gt;</code><code>1</code>

然後通過ifle進行判斷,如果前面fcmpl的結果是&lt; =0的話,則跳轉到11行處的位元組碼去繼續執行。

這個例子還有一個地方和前面不同的是,它隻在方法末有一個return語句,是以在if代碼塊的最後,會有一個goto語句來跳過else塊。goto語句會跳轉到第13條位元組碼處,然後通過iload_3将存儲在局部變量區第三個位置的結果壓入棧中,然後就可以通過return指令将結果傳回了。

Java位元組碼淺析(二)

除了比較數值的指令外,還有比較引用是否相等的(==),以及引用是否等于null的(== null或者!=null),以及比較對象的類型的(instanceof)。

if_icmp&lt;cond&gt;

這組指令用來比較操作數棧頂的兩個整數,然後跳轉到新的位置去執行。&lt;cond&gt;可以是:eq-等于,ne-不等于,lt-小于,le-小于等于,gt-大于, ge-大于等于。

if_acmp&lt;cond&gt;

這兩個指令用來比較對象是否相等,然後根據操作數指定的位置進行跳轉。

ifnonnull ifnull

這兩個指令用來判斷對象是否為null,然後根據操作數指定的位置進行跳轉。

lcmp

這個指令用來比較棧頂的兩個長整型,然後将結果值壓入棧中: 如果value1&gt;value2,壓入1,如果value1==value2,壓入0,如果value1&lt;value2壓入-1.

fcmp&lt;cond&gt; l g dcomp&lt;cond&gt;

這組指令用來比較兩個float或者double類型的值,然後然後将結果值壓入棧中:如果value1&gt;value2,壓入1,如果value1==value2,壓入0,如果value1&lt;value2壓入-1. 指令可以以l或者g結尾,不同之處在于它們是如何處理nan的。fcmpg和dcmpg指令把整數1壓入操作數棧,而fcmpl和dcmpl把-1壓入操作數棧。這確定了比較兩個值的時候,如果其中一個不是數字(not a number, nan),比較的結果不會相等。比如判斷if x &gt; y(x和y都是浮點數),就會用的fcmpl,如果其中一個值是nan的話,-1會被壓入棧頂,下一條指令則是ifle,如果分支小于0則跳轉。是以如果有一個是nan的話,ifle會跳過if塊,不讓它執行。

instanceof

如果棧頂對象的類型是指定的類的話,則将1壓入棧中。這個指令的操作數指定的是某個類型在常量池的序号。如果對象為空或者不是對應的類型,則将0壓入操作數棧中。

if&lt;cond&gt;

将棧頂值和0進行比較,如果條件為真,則跳轉到指定的分支繼續執行。這些指令通常用于較複雜的條件判斷中,在一些單條指令無法完成的情況。比如驗證方法調用的傳回值。

java switch表達式的類型隻能是char,byte,short,int,character, byte, short,integer,string或者enum。jvm為了支援switch語句,用了兩個特殊的指令,叫做tableswitch和lookupswitch,它們都隻能操作整型數值。隻能使用整型并不影響,因為char,byte,short和enum都可以提升成int類型。java7開始支援string類型,下面我們會介紹到。tableswitch操作會比較快一些,不過它消耗的記憶體會更多。tableswitch會列出case分支裡面最大值和最小值之間的所有值,如果判斷的值不在這個範圍内則直接跳轉到default塊執行,case中沒有的值也會被列出,不過它們同樣指向的是default塊。拿下面的這個switch語句作為例子:

<code>public</code> <code>int</code> <code>simpleswitch(</code><code>int</code> <code>intone) {</code>

<code>    </code><code>switch</code> <code>(intone) {</code>

<code>        </code><code>case</code> <code>0</code><code>:</code>

<code>            </code><code>return</code> <code>3</code><code>;</code>

<code>        </code><code>case</code> <code>1</code><code>:</code>

<code>            </code><code>return</code> <code>2</code><code>;</code>

<code>        </code><code>case</code> <code>4</code><code>:</code>

<code>            </code><code>return</code> <code>1</code><code>;</code>

<code>        </code><code>default</code><code>:</code>

<code>            </code><code>return</code> <code>-</code><code>1</code><code>;</code>

<code>12</code>

編譯後會生成如下的位元組碼

<code> </code><code>1</code><code>: tableswitch   {</code>

<code>         </code><code>default</code><code>:</code><code>42</code>

<code>             </code><code>min:</code><code>0</code>

<code>             </code><code>max:</code><code>4</code>

<code>               </code><code>0</code><code>:</code><code>36</code>

<code>               </code><code>1</code><code>:</code><code>38</code>

<code>               </code><code>2</code><code>:</code><code>42</code>

<code>               </code><code>3</code><code>:</code><code>42</code>

<code>               </code><code>4</code><code>:</code><code>40</code>

<code>36</code><code>: iconst_3</code>

<code>13</code>

<code>37</code><code>: ireturn</code>

<code>14</code>

<code>38</code><code>: iconst_2</code>

<code>15</code>

<code>39</code><code>: ireturn</code>

<code>16</code>

<code>40</code><code>: iconst_1</code>

<code>17</code>

<code>41</code><code>: ireturn</code>

<code>18</code>

<code>42</code><code>: iconst_m1</code>

<code>19</code>

<code>43</code><code>: ireturn</code>

tableswitch指令裡0,1,4的值和代碼裡的case語句一一對應,它們指向的是對應代碼塊的位元組碼。tableswitch指令同樣有2,3的值,但代碼中并沒有對應的case語句,它們指向的是default代碼塊。當這條指令執行的時候,會判斷操作數棧頂的值是否在最大值和最小值之間。如果不在的話,直接跳去default分支,也就是上面的42行處的位元組碼。為了確定能找到default分支,它都是出現在tableswitch指令的第一個位元組(如果需要記憶體對齊的話,則在補齊了之後的第一個位元組)。如果棧頂的值在最大最小值的範圍内,則用它作為tableswtich内部的索引,定位到應該跳轉的分支。比如1的話,就會跳轉至38行處繼續執行。下圖會示範這條指令是如何執行的:

Java位元組碼淺析(二)

如果case語句裡面的值取值範圍太廣了(也就是太分散了)這個方法就不太好了,因為它占用的記憶體太多了。是以當switch的case條件裡面的值比較分散的時候,就會使用lookupswitch指令。這個指令會列出case語句裡的所有跳轉的分支,但它沒有列出所有可能的值。當執行這條指令的時候,棧頂的值會和lookupswitch裡的每個值進行比較,來确定要跳轉的分支。執行lookupswitch指令的時候,jvm會在清單中查找比對的元素,這和tableswitch比起來要慢一些,因為tableswitch直接用索引就定位到正确的位置了。當switch語句編譯的時候,編譯器必須去權衡記憶體的使用和性能的影響,來決定到底該使用哪條指令。下面的代碼,編譯器會生成lookupswitch語句:

<code>        </code><code>case</code> <code>10</code><code>:</code>

<code>        </code><code>case</code> <code>20</code><code>:</code>

<code>        </code><code>case</code> <code>30</code><code>:</code>

生成後的位元組碼如下:

<code> </code><code>1</code><code>: lookupswitch  {</code>

<code>           </code><code>count:</code><code>3</code>

<code>              </code><code>10</code><code>:</code><code>36</code>

<code>              </code><code>20</code><code>:</code><code>38</code>

<code>              </code><code>30</code><code>:</code><code>40</code>

<code>36</code><code>: iconst_1</code>

<code>40</code><code>: iconst_3</code>

為了確定搜尋算法的高效(得比線性查找要快),這裡會提供清單的長度,同時比對的元素也是排好序的。下圖示範了lookupswitch指令是如何執行的。

Java位元組碼淺析(二)

未完待續。