Java——常量池探索
概念
什麼是常量?
對於這個問題,可能很多人都可以脫口而出 : 用final修飾的變數是常量 ,或者是在編譯時期定義好的字串。(字串常量)
但是這種說法是不嚴謹的,因為準確來說 : 常量是用final修飾的成員變數!常量在類編譯時期載入類的常量池中。
即final修飾的成員變數(例項變數)和靜態變數(靜態變數也只能是用static修飾的成員變數),那麼用final修飾的區域性變數(方法內)我們也可以稱之為不可變變數。(儲存在棧中)
常量池
Java中的常量池,實際上分為兩種形態:靜態常量池和執行時常量池。
靜態常量池 : *.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,佔用class檔案絕大部分空間。(編譯時期)
執行時常量池 :jvm虛擬機器在完成類裝載操作後,將class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池。(執行時期)
補充 : 執行時常量池中的常量,基本來源於各個class檔案中的常量池。(即每個class檔案都有對應的常量池)
常量池的好處
常量池是為了避免頻繁的建立和銷燬物件而影響系統性能,其實現了物件的共享。例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中。
(1)節省記憶體空間:常量池中所有相同的字串常量被合併,只佔用一個空間。
(2)節省執行時間:比較字串時,==比equals()快。對於兩個引用變數,只用==判斷引用是否相等,也就可以判斷實際值是否相等
雙等號==的含義
基本資料型別之間應用雙等號,比較的是他們的數值。
複合資料型別(類)之間應用雙等號,比較的是他們在記憶體中的存放地址。(引用地址)
String hello="helloMoto";
String hello2="helloMoto";
例如我們定義hello和hello2,並且字串常量池中沒有存在”helloMoto”這個字串常量。
那麼首先會在字串常量池中建立”helloMoto”字串物件,hello指向字串常量池中”helloMoto”字串物件。
第一行程式碼,hello2首先會去常量池中尋找是否有”helloMoto”,發現已經存在,就直接指向該字串常量池中”helloMoto”字串物件。(String物件探索)
Class類檔案中的常量池
魔數 : 每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。很多檔案儲存標準中都使用魔數來表示身份識別。使用魔數而不是副檔名來進行識別主要是基於安全方面的考慮,因為副檔名可以隨意的改動。Class檔案的魔數有很浪漫的氣息,值為0xCAFEBABE這也是java是咖啡圖示和商標名的原因之一。
版本號 : 緊接著4個魔數字節後面儲存的是Class檔案的版本號:第5和6個位元組是次版本號,第7和第8個位元組是主版本號。
常量池 : 接著主次版本號之後的是常量池入口,常量池可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一。(Class類檔案中的常量池在類未載入到記憶體中可以稱為靜態常量池) 。入口處用2個位元組標識常量池常量數量。
我們使用十六進位制編輯器WinHex
開啟Class檔案
public class Test2 {
public static void main(String[] args) {
String hello="helloMoto";
}
}
常量池中存放了各種型別的常量,他們都有自己的型別,並且都有自己的儲存規範,本文只關注字串常量,字串常量以01開頭(1個位元組),接著用2個位元組記錄字串長度,然後就是字串實際內容。
常量池
常量池主要用於存放兩大類常量:字面量和符號引用量
字面量相當於Java語言層面常量的概念,如文字字串,宣告為final的常量值(成員變數)等。
符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:
類和介面的全限定名
欄位名稱和描述符
方法名稱和描述符
執行時常量池
在Class類檔案中不會儲存各個方法、欄位的最終記憶體佈局資訊,因此這些欄位、方法的符號引用如不過不經過執行期轉換的話無法得到真正的記憶體入口地址,也就無法直接被虛擬機器使用。這部分內容將在類載入後進入方法區的執行時常量池中存放。
執行時常量池相對於CLass檔案常量池(靜態常量池)的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入CLass檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
包裝類常量池(物件池)
java中基本型別的包裝類的大部分都實現了常量池技術,
即Byte
,Short
,Integer
,Long
,Character
,Boolean
;Float,Double
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1==i2);//true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3==i4);//false
對於上面2段程式碼不同結果我們可以追溯Integer
原始碼
//Integer
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到如果值位於[-128,127]區域中,會使用IntegerCache
類快取資料,類似於字串常量池。
所以如果賦的值超出這個區域, 便會建立一個新的Integer
物件。(好處是平時如果頻繁的使用Integer,並且數值在[-128,127]中,便不會重複建立新的Integer
物件)
但是Double
和Float
這兩個基本資料型別的包裝類就沒有對應常量池(物件池)的實現。
//Double
public static Double valueOf(double d) {
return new Double(d);
}
Java中裝箱和拆箱
基本資料型別 | 包裝類 |
---|---|
int(4位元組) | Integer |
byte(1位元組) | Byte |
short(2位元組) | Short |
long(8位元組)) | Long |
float(4位元組) | Float |
double(8位元組) | Double |
char(2位元組) | Character |
boolean(1位元組) | Boolean |
賦值時
- 裝箱
如果要生成一個數值為10的Integer物件,只需要這樣:
Integer i = 10;
這個過程中會自動根據數值建立對應的 Integer物件,這就是裝箱。
- 拆箱
Integer i = 10; //裝箱
int n = i; //拆箱
簡單一點說,裝箱就是 自動將基本資料型別轉換為包裝器型別;拆箱就是 自動將包裝器型別轉換為基本資料型別。
方法呼叫時
public class Test2 {
public static void main(String[] args) {
int result = print(5);//int值 5 轉換成對應的Integer物件(裝箱)
}
private static int print(Integer a) {//接收Integer物件作為引數
System.out.println("a==" + a);
return a;//返回int 型別,Integer自動拆箱轉為int型別。
}
}
//a==5
方法運算時
public class Test2 {
public static void main(String[] args) {
Integer sum = 0;
for (int i = 1000; i < 5000; i++) {
//自動拆箱為int型別才能運算
//運算結果再自動裝箱為Integer型別
sum += i;
}
}
}
上面的程式碼sum+=i可以看成sum = sum + i,但是+這個操作符不適用於Integer物件,首先sum進行自動拆箱操作,進行數值相加操作,最後發生自動裝箱操作轉換成Integer物件。其內部變化如下
int result = sum.intValue() + i;
Integer sum = new Integer(result);
由於我們這裡宣告的sum為Integer型別,在上面的迴圈中會建立將近4000個無用的Integer物件,在這樣龐大的迴圈中,會降低程式的效能並且加重了垃圾回收的工作量。因此在我們程式設計時,需要注意到這一點,正確地宣告變數型別,避免因為自動裝箱引起的效能問題。