引言
本文將介紹常量池 與 裝箱拆箱機制,之所以將兩者合在一起介紹,是因為網上不少文章在談到常量池時,將包裝類的快取機制,java常量池,不加區別地混在一起討論,更有甚者完全將這兩者視為一個整體,給初學者帶來不少困擾,我就是過來的。同時,也因為包裝類的快取 與 字串常量池的思想是一樣的,很容易混淆,但是實現方式是不一樣的。
一、常量池
在介紹常量池前,先來介紹一下常量、字面常量、符號常量的定義。
常量 可分為 字面常量(也稱為直接常量)和 符號常量。
字面常量: 是指在程式中無需預先定義就可使用的數字、字元、boolen值、字串等。簡單的說,就是確定值的本身。如 10,2L,2.3f,3.5,“hello”,'a',true、false、null 等等。
符號常量: 是指在程式中用識別符號預先定義的,其值在程式中不可改變的量。如 final int a = 5
;
常量池
常量池引入的 目的 是為了避免頻繁的建立和銷燬物件而影響系統效能,其實現了物件的共享。這是一種 享元模式 的實現。
二、 java常量池
Java的常量池可以細分為以下三類:
- 量池,編譯階段)
- 執行時常量池(又稱動態常量池,執行階段)
- 字串常量池(全域性的常量池)
1. class檔案常量池
class檔案常量池,也被稱為 靜態常量池 ,它是.class檔案所包含的一項資訊。用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。 常量池在.class檔案的位置
字面量: 就是上面所說的字面常量。 符號引用: 是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用 一般是指向方法區的本地指標,相對偏移量或是一個能間接定位到目標的控制程式碼)。符號引用可以看作是一個虛擬地址,只有在JVM載入完類,確認了字面量的地址,才會將 符號引用 換成 直接引用。一般包括下面三類常量:- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
常量池的資訊
2. 執行時常量池
執行時常量池,又稱為 動態常量池 ,是JVM在完成載入類之後將class檔案中常量池載入到記憶體中,並儲存在方法區中。也就是說,執行時常量池中的常量,基本來源於各個class檔案中的常量池。 執行時常量池相對於CLass檔案常量池的另外一個重要特徵是具備 動態性 ,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入CLass檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
jvm在執行某個類的時候,必須經過載入、連線、初始化,而連線又包括驗證、準備、解析三個階段。而當類載入到記憶體中後,jvm就會將class常量池中的內容存放到執行時常量池中,也就是說,每個class對應執行時常量池中的一個獨立空間,每個class檔案存放的位置互不干擾。而在解析階段,就會將符號引用替換成對應的直接引用。 不過,String型別 的字面常量要注意:並不是直接在堆上分配空間來建立物件的,JVM為String 字串額外維護了一個常量池 字串常量池,所以遇到字串常量是要先去字串池中尋找是否有重複,如果有,則返回對應的引用。否則,才建立並新增到字串常量池中。換句話說,對於String型別的字面常量,必須要在 字串常量池 中維護一個全域性的引用。
3. 字串常量池(string pool也有叫做string literal pool)
字串常量池儲存的就是字串的字面常量。詳細一點,字串常量池裡的內容是在類載入完成,經過驗證,準備階段之後在堆中生成字串物件例項,然後將該字串物件例項的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的例項物件,具體的例項物件是在堆中開闢的一塊空間存放的。)。 在HotSpot VM裡實現的string pool功能的是一個StringTable類,它是一個雜湊表,裡面存的是駐留字串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字串例項本身),也就是說在堆中的某些字串例項被這個StringTable引用之後就等同被賦予了”駐留字串”的身份。這個StringTable在每個HotSpot VM的例項只有一份,被所有的類共享。
執行時常量池 與 字串常量池 的區別
字串常量池是位於執行時常量池中的。
網上有不少文章是將字串常量池作為執行時常量池同等來說,我一開始也以為這兩者就是同一個東西,其實不然。執行時常量池 與 字串常量池 在HotSpot的JDK1.6以前,都是放在方法區的,JDK1.7就將字串常量池移到了堆外記憶體中去。執行時常量池 為每一個Class檔案的常量池提供一個執行時的記憶體空間;而字串常量池則為所有Class檔案的String型別的字面常量維護一個公共的常量池,也就是Class檔案的常量池載入進執行時常量池後,其String字面常量的引用指向要與字串常量池的維護的要一致。
我們來幾個例子理解一下常量池
@ Example 1 簡單的例子
public class Test_6 {
public static void main(String[] args) {
String str = "Hello World!";
}
}
複製程式碼
我們使用使用javap -v MyTest.class 檢視class檔案的位元組碼,經javap 處理可以輸出我們能看懂的資訊。如下圖:
class檔案的索引#16位置(第16個常量池項)儲存的是 一個描述了字串字面常量資訊(型別,以及內容索引)的資料結構體,這個結構體被稱為CONSTANT_String_info。這個結構體並沒有儲存字串的內容,而是儲存了一個指向字串內容的索引--#17,即第17項儲存的是Hello World 的二進位制碼。@ Example 2 String的+運算例子
我們再來看一個比較複雜的例子
public class Test_6 {
public static void main(String[] args) {
String str_aa = "Love";
String str_bb = "beautiful" + " girl";
String str_cc = str_aa+" China";
}
}
複製程式碼
同樣,檢視class檔案的位元組碼資訊:
class檔案的常量池儲存了Love
、beautiful girl
、China
,但卻沒有 Love China
。為什麼 str_bb 與 str_cc 都是通過 + 連結得到的,為什麼str_cc的值沒有出現在常量池中,而str_bb的值卻出現了。
這是因為str_bb的值是由兩個常量計算得到的,這種只有常量的表示式計算在編譯期間由編譯器計算得到的,要記住,能由編譯器完成的計算,就不會拖到執行期間來計算。
而str_cc的計算中包含了變數str_aa,涉及到變數的表示式計算都是在執行期間計算的,因為變數是無法在編譯期間確定它的值,特別是多執行緒下,同時得到結果是CPU動態分配空間儲存的,也就是說地址也無法確定。我們再去細看,就會發現常量池中的包含了StringBuilder
以及其方法的描述資訊,其實,這個StringBuilder
是為了計算str_aa+" China
"表示式,先呼叫append()
方法,新增兩個字串,在呼叫toString()
方法,返回結果。也就是說,在執行期間,String字串通過 + 來連結的表示式計算都是通過建立StringBuilder來完成的
@ Example 3 String新建物件例子
下面的例子,str_bb的值是直接通過new新建一個物件,觀察靜態常量池。
public class MyTest {
public static void main(String[] args) {
String str_bb = new String("Hello");
}
}
複製程式碼
檢視對應class檔案的位元組碼資訊:
通過new新建物件的操作是在執行期間才完成的,為什麼這裡仍舊在class檔案的常量池中出現呢?這是因為"Hello"本身就是一個字面常量,這是很容易讓人忽略的。有雙引號包裹的都是字面常量。同時,new建立一個String字串物件,確實是在執行時完成的,但這個物件將不同於字串常量池中所維護的常量。二、自動裝箱拆箱機制 與 快取機制
先來簡單介紹一下自動裝箱拆箱機制
1、自動裝拆箱機制介紹
裝箱: 可以自動將基本型別直接轉換成對應的包裝型別。 拆箱: 自動將包裝型別轉換成對應的基本型別值;
//普通的建立物件方式
Integer a = new Integer(5);
//裝箱
Integer b = 5;
//拆箱
int c = b+5;
複製程式碼
2. 自動裝箱拆箱的原理
裝箱拆箱究竟是是怎麼實現,感覺有點神奇,居然可以使基本型別與包裝型別快速轉換。我們再稍微簡化上面的例子:
public class Test_6 {
public static void main(String[] args) {
//裝箱
Integer b = 5;
//拆箱
int c = b+5;
}
}
複製程式碼
依舊使用 javap -v Test_6.class 檢視這個類的class檔案的位元組碼資訊,如下圖:
可以從class的位元組碼發現,靜態常量池中,由Integer.valueOf()
和 Integer.initValue()
這兩個方法的描述。這就有點奇怪,例子中的程式碼中並沒有呼叫這兩個方法,為什麼編譯後會出現呢?
感覺還是不夠清晰,我們換另一種反編譯工具來反編譯一下,這次我們反編譯回java程式碼,使用命令 jad Test_6.class ,得到的反編譯程式碼如下:
public class Test_6
{
public static void main(String args[])
{
Integer b = Integer.valueOf(5);
int c = b.intValue() + 5;
}
}
複製程式碼
這回就非常直觀明瞭了。所謂裝箱拆箱並沒有多厲害,還是要通過呼叫Integer.valueOf()
(裝箱) 和 Integer.initValue()
(拆箱)來完成的。也就是說,自動裝箱拆箱機制是一種語法簡寫,為了方便程式設計師,省去了手動裝箱拆箱的麻煩,變成了自動裝箱拆箱
判別是裝箱還是拆箱
在下面的兩個例子中,可能會讓你很迷惑:不知道到底使用了裝箱,還是使用了拆箱。
Integer x = 1;
Integer y = 2;
Integer z = x+y;
複製程式碼
這種情況其實只要仔細想一下便可以知道:這是 先拆箱再裝箱。因為Integer型別是引用型別,所以不能參與加法運算,必須拆箱成基本型別來求和,在裝箱成Integer。如果改造上面的例子,把Integer變成Short,則正確程式碼如下:
Short a = 5;
Short b = 6;
Short c = (short) (a+b);
複製程式碼
3. 包裝類的快取機制
我們先來看一個例子
public class MyTest {
public static void main(String[] args) {
Integer a = 5;
Integer b = 5;
Integer c = 129;
Integer d = 129;
System.out.println("a==b "+ (a == b));
System.out.println("c==d "+ (c == d));
}
}
複製程式碼
執行結果:
a == b true c == d false
咦,為什麼是a和b所指向的是一個物件呢?難道JVM在類載入時也為包裝型別維護了一個常量池?如果是這樣,為什麼變數c、d的地址不一樣。事實上,JVM確實沒有為包裝類維護一個常量池。變數a、b、c、d是由裝箱得到的,根據前面所說的,裝箱其實是編譯器自動新增了Integer.valueOf()
方法。祕密應該就在這個方法內,那麼我們看一下Integer.valueOf()
的原始碼吧,如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
複製程式碼
程式碼很簡單,判斷裝箱所使用的基本型別值是否在 [ IntegerCache.low
, IntegerCache.high
] 的範圍內,如果在,返回IntegerCache.cache
陣列中對應下標的元素。否則,才新建一個物件。我們繼續深入檢視 IntegerCache
的原始碼,如下:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
//獲取上限值
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
//建立陣列
cache = new Integer[(high - low) + 1];
int j = low;
//填充陣列
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
複製程式碼
從原始碼中,可以知道,IntegerCache.cache
是一個final的Integer陣列,這個陣列儲存的Integer物件元素的值範圍是[-128,127]。而且這個陣列的初始化程式碼是包裹在static程式碼塊中,也就是說IntegerCache.cache
陣列的初始化是在類載入時完成的。
再看回上面的例子,變數a和b的使用的基本型別值為5,超出[-128,127]的範圍,所以就使用快取陣列中的元素,所以a、b的地址是一樣的。而c、d使用的基本型別值為129,超出快取範圍,所以都是各自在堆上建立一個對,地址自然就不一樣了。
包裝類快取總結與補充:
- 包裝類與String類很相似,都是非可變類,即一經建立後,便不可以修改。正因為這種特性,兩者的物件例項在多執行緒下是安全的,不用擔心非同步修改的情況,這為他們實現共享提供了很好的保證,只需建立一個物件共享便可。
- 包裝類的共享實現並不是由JVM來維護一個常量池,而是使用了快取機制(陣列),而且這個快取是在類載入時完成初始化,並且不可再修改。
- 包裝類的陣列快取範圍是有限,只快取基本型別值在一個位元組範圍內,也就是說 -128 ~ 127。(Character的範圍是 0~127)
- 目前並不是所有包裝類都提供快取機制,只有Byte、Character、Short、Integer 4個包裝類提供,Long、Float、Double 不提供。
出處:http://www.cnblogs.com/jinggod/p/8425748.html
文章有不當之處,歡迎指正,你也可以關注我的微信公眾號:
好好學java
,獲取優質資源。