天天看點

Java的基本資料類型在虛拟機中的實作

前言

首先我們做個小測驗,通過下面代碼來看看Java語言和Java虛拟機對boolean類型有什麼不同:

public class Foo {
   public static void main(String[] args) {
      boolean flag = true;
      if (flag) System.out.println("Hello, Java!");
      if (flag == true) System.out.println("Hello, JVM!");
   }
}
           

我們編譯執行後的輸出結果如下:

$ javac Foo.java 
$ java Foo
$ Hello, Java!
  Hello, JVM!
           

下面我們通過asmtools将虛拟機中flag的值改為2,我們再看看輸出結果。

$ java -cp /Users/leiqi/Library/sdk/asmtools.jar  org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
$ awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
$ java -cp /Users/leiqi/Library/sdk/asmtools.jar  org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
$ Hello, Java!
           

這次我們看到輸出結果跟上次不一樣了。那麼我們可能會想到:當一個boolean類型的值為2時,它究竟是true還是false?

下面我們詳細分析一下它背後的邏輯。

Java虛拟機的boolean類型

首先,我們看看Java語言規範和Java虛拟機規範中分别怎麼定義boolean類型。

在Java語言規範中boolean類型的值隻有兩種可能,

true

或者

false

。 但是這兩者不能直接被虛拟機引用。

在Java虛拟機規範中,boolean類型的值被映射為int類型。

true

被映射為1,

false

被映射為0。這個編碼映射規則限制了Java位元組碼的具體實作。也就是說:對于存儲boolean數組的位元組碼 ,虛拟機需要保證存入的是1或0

Java虛拟機同時也要求Java編譯器對應也得遵守這個規則,并且用整數相關的位元組碼來實作邏輯運算,以及基于boolean類型的條件跳轉。這樣一來,在編譯而成的class檔案中,除了字段和傳入的參數外,基本看不出boolean類型的痕迹。

# Foo.main 編譯後的位元組碼 
0: iconst_2 // 我們用 AsmTools更改了這一指令
1: istore_1
2: iload_1
3: ifeq 14 // 第一個 if 語句,即操作數棧上數值為 0 時跳轉
6: getstatic java.lang.System.out
9: ldc "Hello, Java!"
11: invokevirtual java.io.PrintStream.println
14: iload_1
15: iconst_1
16: if_icmpne 27 // 第二個if語句,即操作數棧上兩個數值不相同時跳轉
19: getstatic java.lang.System.out
22: ldc "Hello, JVM!"
24: invokevirtual java.io.PrintStream.println
27: return

           

在前面的例子中,第一個if語句會被編譯成條件跳轉位元組碼ifeq,翻譯成人話就是說,如果局部變量

flag

的值為0,那麼不列印“Hello Java!”。

而第二個if語句則會編譯為條件跳轉位元組碼if_cmpne,也就是說,如果局部變量的值和整數1相等,則列印“Hello JVM!” ,否則跳過這句。

可以看到,Java 編譯器的确遵守了相同的編碼規則。當然,這個限制很容易繞開。除了我們之前用得到的AsmTools外,還有一些工具可以修改位元組碼的java庫,如ASM等。

對于JAVA虛拟機來說,他看到的boolean類型,早已被映射為整數類型。是以,将原本聲明為boolean類型的局部變量,指派為除了0,1之外的整數值,在Java虛拟機看來是“合法”的。

在之前的例子中,經過編譯器編譯之後,java虛拟機看到的不是在問“flag是否非零”,而是變成在問“flag為幾”。也就是第一個if語句變成:flag的值為0嗎?第二個if語句則變成:flag的值是1嗎?

如果約定俗成,flag隻能為0/1,那麼第二個if語句還是有意義的。但如果我們打破正常,flag的值為大于1,那麼較真的Java虛拟機就會将第二個if語句判定為假。

Java的基本類型

除了上面提到的boolean類型外,Java的基本類型還包括byte、short、char、int、long、float以及double。

Java的基本類型都有對應的值域和預設值。可以看到,byte、short、int、long、float以及double的值域依次擴大,而且前面的值域被後面的值域所包含。是以,從前面的基本類型轉換至後面的基本類型,無需強制轉換。另外一點值得注意的是,盡管他們的預設值看起來不一樣,但是在記憶體中都是0。

在這些基本類型中,boolean和char是唯二的無符号類型。在不考慮違反規範的情況下,boolean類型的取值範圍是0或者1.char類型的取值範圍是[0,65535]。通常我們可以認定char類型的值為負數。這種特性十分有用,比如說作為數組索引等。

在前面的例子中,我們能夠将整數2存儲到一個聲明為boolean類型的局部變量中。那麼,聲明為byte,char以及short的局部變量,是否也能夠存儲超出它們範圍的數值昵?

答案是可以的。而且,這些超出取值範圍的數值依然會帶來一些麻煩。比如說,聲明為char類型的局部變量實際上有可能為負數。當然,在政策使用Java編譯器的情況下,生成的位元組碼會遵守Java虛拟機規範對編譯器的限制,是以你無須過分擔心局部變量會超出它們的取值範圍。

Java的浮點類型彩印IEEE 754 浮點數格式。以float為例,浮點類型通常有兩個0,+0.0F以及-0.0F。

前者在Java裡是0,後者是符号位為1,其他位均為0的浮點數,在記憶體中等同于十六進制整數0x8000000(即-0.0F可通過Float.intBitsToFloat(0x8000000)求得)。盡管它們的記憶體數值不同,但是在Java中 +0.0F == -0.0F會傳回為真。

在有了+0.0F 和 -0.0F這兩個定義以後,我們便可以定義浮點數中的正無窮及負無窮。正無窮就是任意正浮點數(不包括+0.0F)除以+0.0F得到的值,而負無窮就是任意負浮點數(不包括-0.0F)除以-0.0F得到的值。 在Java中,正無窮和負無窮是有确切的值,在記憶體中分别等同于十六進制整數0x7F800000 和 0xFF800000。

而 [0x7F800001, 0x7FFFFFFF] 和 [0xFF800001, 0xFFFFFFFF] 對應的都是NaN(Not-a-Number) 。當然,一般我們計算得出的NaN,比如說通過+0.0F/+0.0F,在記憶體中應為0x7FC00000。這個數字,我們都稱之為标準的NaN,而其他的我們稱之為不标準的NaN。

NaN有一個有趣的特性:除了"!="始終傳回true之外,所有其它比較結果都會傳回false。

舉例來說,

  • NaN < 1.0F -> false
  • NaN >= 1.0F -> false
  • f != NaN -> true (f為任意浮點數)
  • f == NaN -> false (f為任意浮點數)

是以,我們在程式裡做浮點數比較時,需要考慮上述特性。

Java基本類型的大小

前面有提到 Java虛拟機每調用一個方法,便會建立一個棧幀 為了友善了解,這裡我隻讨論供解釋器使用的解釋棧幀(interpreted frame)。

這種棧幀有兩個主要的組成部分,分别是局部變量區,以及位元組碼的操作數棧。這裡的局部變量是廣義的,除了普遍意義下的局部變量之外,它還包含執行個體方法的"this指針"以及方法所接受的參數。

在Java虛拟機規範中,局部變量區等價于一個數組,并且可以用正整數來索引。除了long、double值需要用兩個贖罪單元來存儲外,其它基本類型以及引用類型的值均占用一個數組單元。

也就是說,boolean、byte、char、short 這四種類型,在棧上占用的空間和 int 是一樣的,和引用類型也是一樣的。是以,在 32 位的 HotSpot 中,這些類型在棧上将占用 4 個位元組;而在 64 位的 HotSpot 中,他們将占 8 個位元組。

當然,這種情況僅存在于局部變量,而并不會出現在存儲于堆中的字段或者數組元素上。對于 byte、char 以及 short 這三種類型的字段或者數組單元,它們在堆上占用的空間分别為一位元組、兩位元組,以及兩位元組,也就是說,跟這些類型的值域相吻合。

是以,當我們将一個 int 類型的值,存儲到這些類型的字段或數組時,相當于做了一次隐式的掩碼操作。舉例來說,當我們把 0xFFFFFFFF(-1)存儲到一個聲明為 char 類型的字段裡時,由于該字段僅占兩位元組,是以高兩位的位元組便會被截取掉,最終存入“\uFFFF”。

boolean 字段和 boolean 數組則比較特殊。在 HotSpot 中,boolean 字段占用一位元組,而 boolean 數組則直接用 byte 數組來實作。為了保證堆中的 boolean 值是合法的,HotSpot 在存儲時顯式地進行掩碼操作,也就是說,隻取最後一位的值存入 boolean 字段或數組中。

講完了存儲,現在我來講講加載。Java 虛拟機的算數運算幾乎全部依賴于操作數棧。也就是說,我們需要将堆中的 boolean、byte、char 以及 short 加載到操作數棧上,而後将棧上的值當成 int 類型來運算。

對于 boolean、char 這兩個無符号類型來說,加載伴随着零擴充。舉個例子,char 的大小為兩個位元組。在加載時 char 的值會被複制到 int 類型的低二位元組,而高二位元組則會用 0 來填充。

對于 byte、short 這兩個類型來說,加載伴随着符号擴充。舉個例子,short 的大小為兩個位元組。在加載時 short 的值同樣會被複制到 int 類型的低二位元組。如果該 short 值為非負數,即最高位為 0,那麼該 int 類型的值的高二位元組會用 0 來填充,否則用 1 來填充。

總結

1、boolean 類型在 Java 虛拟機中被映射為整數類型:“true”被映射為 1,而“false”被映射為 0。Java 代碼中的邏輯運算以及條件跳轉,都是用整數相關的位元組碼來實作的。

2、除 boolean 類型之外,Java 還有另外 7 個基本類型。它們擁有不同的值域,但預設值在記憶體中均為 0。這些基本類型之中,浮點類型比較特殊。基于它的運算或比較,需要考慮 +0.0F、-0.0F 以及 NaN 的情況。

3、除 long 和 double 外,其他基本類型與引用類型在解釋執行的方法棧幀中占用的大小是一緻的,但它們在堆中占用的大小确不同。在将 boolean、byte、char 以及 short 的值存入字段或者數組單元時,Java 虛拟機會進行掩碼操作。在讀取時,Java 虛拟機則會将其擴充為 int 類型。

4、将boolean 儲存在靜态域中,指定了其類型為’Z’,當修改為2時取低位最後一位為0,當修改為3時取低位最後一位為1。

則說明boolean的掩碼處理是取低位的最後一位。