java基礎(八) 深入解析常量池與裝拆箱機制
??本文將介紹常量池 與 裝箱拆箱機制,之所以將兩者合在一起介紹,是因為網上不少文章在談到常量池時,將包裝類的緩存機制,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)。
字面量: 就是上面所說的字面常量。
符號引用: 是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用 一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。符號引用可以看作是一個虛擬地址,只有在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
,獲取優質資源。
java基礎(八) 深入解析常量池與裝拆箱機制