1. 常量池的具體結構
在java程序中,有很多的東西是永恆的,不會在運行過程中變化。比如一個類的名字,一個類欄位的名字/所屬類型,一個類方法的名字/返回類型/參數名與所屬類型,一個常量,還有在程序中出現的大量的字面值。
比如下面小段源碼中粗體代碼顯示的部分:
public class ClassTest {
private String itemS ="我們 ";
private final int itemI =100 ;
public void setItemS (String para ){...}
}
而這些在JVM解釋執行程序的時候是非常重要的。那麼編譯器將源程序編譯成class文件後,會用一部分位元組分類存儲這些粗體代碼。而這些位元組我們就稱為常量池。事實上,只有JVM載入class後,在方法區中為它們開辟了空間才更像一個「池」。
正如上面所示,一個程序中有很多永恆的類似粗體代碼顯示的部分。每一個都是常量池中的一個常量表(常量項)。而這些常量表之間又有不同,class文件共有11種常量表,如下所示: 常量表類型 標志值(佔1 byte) 描述 CONSTANT_Utf8 1 UTF-8編碼的Unicode字元串 CONSTANT_Integer 3 int類型的字面值 CONSTANT_Float 4 float類型的字面值 CONSTANT_Long 5 long類型的字面值 CONSTANT_Double 6 double類型的字面值 CONSTANT_Class 7 對一個類或介面的符號引用 CONSTANT_String 8 String類型字面值的引用 CONSTANT_Fieldref 9 對一個欄位的符號引用 CONSTANT_Methodref 10 對一個類中方法的符號引用 CONSTANT_InterfaceMethodref 11 對一個介面中方法的符號引用 CONSTANT_NameAndType 12 對一個欄位或方法的部分符號引用 (1) CONSTANT_Utf8 用UTF-8編碼方式來表示程序中所有的重要常量字元串。這些字元串包括: ①類或介面的全限定名, ②超類的全限定名,③父介面的全限定名, ④類欄位名和所屬類型名,⑤類方法名和返回類型名、以及參數名和所屬類型名。⑥字元串字面值
表格式: tag(標志1:佔1byte) length(字元串所佔位元組的長度,佔2byte) bytes(字元串位元組序列)
(2) CONSTANT_Integer、 CONSTANT_Float、 CONSTANT_Long、 CONSTANT_Double 所有基本數據類型的字面值。比如在程序中出現的1用CONSTANT_Integer表示。3.1415926F用 CONSTANT_Float表示。
表格式: tag bytes(基本數據類型所需使用的位元組序列)
(3) CONSTANT_Class 使用符號引用來表示類或介面。我們知道所有類名都以 CONSTANT_Utf8表的形式存儲。但是我們並不知道 CONSTANT_Utf8表中哪些字元串是類名,那些是方法名。因此我們必須用一個指向類名字元串的符號引用常量來表明。
表格式: tag name_index(給出表示類或介面名的CONSTANT_Utf8表的索引)
(4) CONSTANT_String 同 CONSTANT_Class,指向包含字元串字面值的 CONSTANT_Utf8表。
表格式: tag string_index(給出表示字元串字面值的CONSTANT_Utf8表的索引)
(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含該欄位或方法所屬類名的 CONSTANT_Utf8表,以及指向包含該欄位或方法的名字和描述符的 CONSTANT_NameAndType 表
表格式: tag class _index(給出包含所屬類名的CONSTANT_Utf8表的索引) name_and_type_index(包含欄位名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)
(6) CONSTANT_NameAndType 指向包含欄位名或方法名以及描述符的 CONSTANT_Utf8表。
表格式: tag name_index(給出表示欄位名或方法名的CONSTANT_Utf8表的索引) type_index(給出表示描述符的CONSTANT_Utf8表的索引)
在Java源代碼中的每一個字面值字元串,都會在編譯成class文件階段,形成標志號為8(CONSTANT_String_info)的常量表 。 當JVM載入 class文件的時候,會為對應的常量池建立一個內存數據結構,並存放在方法區中。同時JVM會自動為CONSTANT_String_info常量表中的字元串常量的字面值 在堆中創建新的String對象(intern字元串對象 ,又叫拘留字元串對象)。然後把CONSTANT_String_info常量表的入口地址轉變成這個堆中String對象的直接地址(常量池解析)。
拘留字元串對象
源代碼中所有相同字面值的字元串常量只可能建立唯一 一個拘留字元串對象。 實際上JVM是通過一個記錄了拘留字元串引用的內部數據結構來維持這一特性的。在Java程序中,可以調用String的intern()方法來使得一個常規字元串對象成為拘留字元串對象。
(1)String s=new String("Hello world"); 編譯成class文件後的指令(在myeclipse中查看):
事實上,在運行這段指令之前,JVM就已經為"Hello world"在堆中創建了一個拘留字元串( 值得注意的是:如果源程序中還有一個"Hello world"字元串常量,那麼他們都對應了同一個堆中的拘留字元串)。然後用這個拘留字元串的值來初始化堆中用new指令創建出來的新的String對象,局部變數s實際上存儲的是new出來的堆對象地址。
(2)String s="Hello world";
這跟(1)中創建指令有很大的不同,此時局部變數s存儲的是早已創建好的拘留字元串的堆地址。
java常量池技術 java中的常量池技術,是為了方便快捷地創建某些對象而出現的,當需要一個對象時,就可以從池中取一個出來(如果池中沒有則創建一個),則在需要重復創建相等變數時節省了很多時間。常量池其實也就是一個內存空間,常量池存在於方法區中。
String類也是java中用得多的類,同樣為了創建String對象的方便,也實現了常量池的技術。
測試代碼如下:
public class Test{
public static void main(String[] args){
//s1,s2分別位於棧中,指向堆中不同的空間
String s1=new String("hello");
String s2=new String("hello");
System.out.println(s1==s2);//輸出false
//s3,s4位於池中同一空間
String s3="hello" String s4="hello";
System.out.println(s3==s4);//輸出true
}
}
用new String()創建的字元串不是常量,不能在編譯期就確定,所以new String()創建的字元串不放入常量池中,他們有自己的地址空間。
String 對象(內存)的不變性機制會使修改String字元串時,產生大量的對象,因為每次改變字元串,都會生成一個新的String。 java 為了更有效的使用內存,常量池在編譯期遇見String 字元串時,它會檢查該池內是否已經存在相同的String 字元串,如果找到,就把新變數的引用指向現有的字元串對象,不創建任何新的String 常量對象,沒找到再創建新的。所以對一個字元串對象的任何修改,都會產生一個新的字元串對象,原來的依然存在,等待垃圾回收。
代碼:
String a = 「test」;
String b = 「test」;
String b = b+"java";
a,b同時指向常量池中的常量值"test",b=b+"java"之後,b原先指向一個常量,內容為"test」,通過對b進行+"java" 操作後,b之前所指向的那個值沒有改變,但此時b不指向原來那個變數值了,而指向了另一個String變數,內容為」test java「。原來那個變數還存在於內存之中,只是b這個變數不再指向它了。
八種基本類型的包裝類和對象池 java中基本類型的包裝類的大部分都實現了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數類型的包裝類則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值小於等於127時才可使用常量池,也即對象不負責創建和管理大於127的這些類的對象。 一些對應的測試代碼:
public class Test{ public static void main(String[] args){
//5種整形的包裝類Byte,Short,Integer,Long,Character的對象,
//在值小於127時可以使用常量池
Integer i1=127;
Integer i2=127;
System.out.println(i1==i2); //輸出true
//值大於127時,不會從常量池中取對象
Integer i3=128;
Integer i4=128;
System.out.println(i3==i4); //輸出false
//Boolean類也實現了常量池技術
Boolean bool1=true;
Boolean bool2=true;
System.out.println(bool1==bool2); //輸出true
//浮點類型的包裝類沒有實現常量池技術
Double d1=1.0;
Double d2=1.0;
System.out.println(d1==d2); //輸出false
}
}
對Integer對象的代碼補充
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
當你直接給一個Integer對象一個int值的時候,其實它調用了valueOf方法,然後你賦的這個值很特別,是128,那麼沒有進行cache方法,相當於new了兩個新對象。所以問題中定義a、b的兩句代碼就類似於:
Integer a = new Integer(128);
Integer b = new Integer(128);
這個時候再問你,輸出結果是什麼?你就知道是false了。如果把這個數換成127,再執行:
Integer a = 127;
Integer b = 127;
System.out.println(a == b);
結果就是:true
進行對象比較時最好還是使用equals,便於按照自己的目的進行控制。這里引出equals()和==,equals比較的是字元串字面值即比較內容,==比較引用。
看一下IntegerCache這個類裡面的內容:
private static class IntegerCache {
private IntegerCache() {
}
static final Integer cache[] = new Integer[-(-128) + 127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
}
由於cache[]在IntegerCache類中是靜態數組,也就是只需要初始化一次,即static{......}部分,所以,如果Integer對象初始化時是-128~127的范圍,就不需要再重新定義申請空間,都是同一個對象---在IntegerCache.cache中,這樣可以在一定程度上提高效率。
針對String方面的補充
在同包同類下,引用自同一String對象.
在同包不同類下,引用自同一String對象.
在不同包不同類下,依然引用自同一String對象.
在編譯成.class時能夠識別為同一字元串的,自動優化成常量,所以也引用自同一String對象.
在運行時創建的字元串具有獨立的內存地址,所以不引用自同一String對象.
String的intern()方法會查找在常量池中是否存在一份equal相等的字元串,
如果有則返回一個引用,沒有則添加自己的字元串進入常量池,注意:只是字元串部分。
所以這時會存在2份拷貝,常量池的部分被String類私有並管理,自己的那份按對象生命周期繼續使用。
返回字元串對象的規范化表示形式
一個初始值為空的字元串池,它由類 String 私有地維護。
當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字元串(該對象由 equals(Object) 方法確定),則返回池中的字元串引用。否則,將此 String 對象添加到池中,並且返回此 String 對象的引用。
它遵循對於任何兩個字元串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。
所有字面值字元串和字元串賦值常量表達式都是內部的。
------------------------------------代碼演示補充-------------------------------------
String s0= "java";
String s1=new String("java");
String s2=new String("java");
s1.intern();
s2=s2.intern(); //把常量池中"java"的引用賦給s2
System.out.println( s0==s1);//false 「 intern返回的引用沒有引用變數接收~ s1.intern();等於廢代碼.」
System.out.println( s0==s1.intern() );//true
System.out.println( s0==s2 );//true
------------------------------------代碼演示補充-------------------------------------
String s1=new String("java");
String s2=s1.intern();//s1 檢查常量池,發現沒有就拷貝自己的字元串進去
//s2 引用該字元串常量池的地址
System.out.println(s2 == s1);//false
System.out.println( s2==s1.intern());//true
System.out.println( s1==s1.intern());// false
2. java方法區中包含哪些內容,常量池中包含哪些內容
方法區里存儲著class文件的信息和動態常量池,class文件的信息包括類信息和靜態常量池。可以將類的信息是對class文件內容的一個框架,裡面具體的內容通過常量池來存儲。
動態常量池裡的內容除了是靜態常量池裡的內容外,還將靜態常量池裡的符號引用轉變為直接引用,而且動態常量池裡的內容是能動態添加的。例如調用String的intern方法就能將string的值添加到String常量池中,這里String常量池是包含在動態常量池裡的,但在jdk1.8後,將String常量池放到了堆中。
3. 什麼叫做字元串常量池
可以理解為內存裡面專門為string類型變數開辟的一片區域
譬如String a = "abc"; 當你定義這樣一個變數的時候,java此時先會去常量池尋找有沒有"abc"這樣的字元串,如果有,直接把內存地址交給a, 否則就生成一個"abc"的字元串
當下一個String b = "abc";的時候,發現常量池已經有"abc"了,此時JVM不會再次生成"abc",而是直接交給"abc"引用給b, 所以此時你會發現a == b
4. java 靜態變數和靜態常量
靜態變數是屬於靜態存儲方式,但是屬於靜態存儲方式的量不一定就是靜態變數,例如外部變數雖屬於靜態存儲方式,但不一定是靜態變數,必須由static加以定義後才能成為靜態外部變數,或稱靜態全局變數。
對於自動變數,它屬於動態存儲方式。但是也可以用static定義它為靜態自動變數,或稱靜態局部變數,從而成為靜態存儲方式。由此看來,一個變數可由static進行再說明,並改變其原有的存儲方式。
而在以Pascal為代表的許多程序語言中,所有局部變數都由系統自動分配存儲空間,而所有全局變數的存儲空間則以靜態分配的方式獲取,因此由於實際上「局部變數」和「全局變數」這兩個術語已足以涵蓋所有的情況,在這些程序語言中通常不使用「靜態變數」這一術語,而直接以「全局變數」代之。
在這些程序語言中,靜態變數就是全局變數,而即使在有明確區分全局和靜態變數的程序語言中,在編譯後的代碼里二者也以相同的方式獲取存儲空間。而今術語「靜態變數」的概念則主要基於C族語言的「static」的定義。
靜態變數也可以用於存儲常數。具體來說,靜態變數可用const,constant或final等關鍵字標識,這時其值就會在編譯時設定,並且無法在運行時改變。編譯器通常將靜態常量與文本一起置於目標文件的文本區域,而非常量初始化數據則置於數據區
5. java 常量池 到底是在堆中還是棧中
java常量池不在堆中也不在棧中,是獨立的內存空間管理。
1. 棧:存放基本類擾伏首型的變數數據和對象的引用,但對象本身不存放在棧中,而是存放在堆(new 出來的對象)或者常量池中(字元串常量對象存放在常量池中。)
2. 堆:存放所有new出來的對象。
3. 常量池:存放字元串常量和基本類型常量(public static final)。
對於字元串:其對象的引用都是存儲在棧中的,如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常量池緩數中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字元串,在常廳明量池中永遠只有一份,在堆中有多份。
6. java中String問題,String a=new String(""); 和String a=new String();有區別嗎
詳細的這兒有哇:
解析Java中的String對象的數據類型
1. 首先String不屬於8種基本數據類型,String是一個對象。
因為對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性。
2. new String()和new String(「」)都是申明一個新的空字元串,是空串不是null;
3. String str=」kvill」;
String str=new String (「kvill」);的區別:
在這里,我們不談堆,也不談棧,只先簡單引入常量池這個簡單的概念。
常量池(constant pool)指的是在編譯期被確定,並被搭亮睜保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、介面等中的常量,也包括字元串常量。
看例1:
String s0=」kvill」;
String s1=」kvill」;
String s2=」kv」 + 「ill」;
System.out.println( s0==s1 );
System.out.println( s0==s2 );
結果為:
true
true
首先,我們要知結果為道Java會確保一個字元串常量只有一個拷貝。
因為例子中的s0和s1中的」kvill」都是字元串常量,它們在編譯期就被確定了,所以s0==s1為true;而」kv」和」ill」也都是字元串常量,當一個字元串由多個字元串常量連接而成時,它自己肯定也是字元串常量,所以s2也同樣在編譯期就被解析為一個字元串常量,所以s2也是常量池中」kvill」的一個引用。
所以我們得出s0==s1==s2;
用new String() 創建的字元串不是常量,不能在編譯期就確定,所以new String() 創建的字元串不放入常量池中,它們有自己的地址空間。
看例2:
String s0=」kvill」;
String s1=new String(」kvill」);
String s2=」kv」 + new String(「ill」);
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );
結果為:
false
false
false
例2中s0還是常量池中」kvill」鍵滑的應用,s1因為無法在編譯期確定,所以是運行時創建的新對象」kvill」的引用,s2因為有後半部分new String(「ill」)所以也無法在編譯期確定,所以也是一個新創建對象」kvill」的應用;明白了這些也就知道為何得出此結果了。
4. String.intern():
再補充介紹一點:存在於.class文件中的常量池,在運行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字元串知歲常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字元串並返回它的引用;看例3就清楚了
例3:
String s0= 「kvill」;
String s1=new String(」kvill」);
String s2=new String(「kvill」);
System.out.println( s0==s1 );
System.out.println( 「**********」 );
s1.intern();
s2=s2.intern(); //把常量池中「kvill」的引用賦給s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );
結果為:
false
**********
false //雖然執行了s1.intern(),但它的返回值沒有賦給s1
true //說明s1.intern()返回的是常量池中」kvill」的引用
true
最後我再破除一個錯誤的理解:
有人說,「使用String.intern()方法則可以將一個String類的保存到一個全局String表中,如果具有相同值的 Unicode字元串已經在這個表中,那麼該方法返回表中已有字元串的地址,如果在表中沒有相同值的字元串,則將自己的地址注冊到表中「如果我把他說的這個全局的String表理解為常量池的話,他的最後一句話,「如果在表中沒有相同值的字元串,則將自己的地址注冊到表中」是錯的:
看例4:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
結果:
false
kvill kvill
true
在這個類中我們沒有聲名一個」kvill」常量,所以常量池中一開始是沒有」kvill」的,當我們調用s1.intern()後就在常量池中新添加了一個」kvill」常量,原來的不在常量池中的」kvill」仍然存在,也就不是「將自己的地址注冊到常量池中」了。
s1==s1.intern()為false說明原來的「kvill」仍然存在;
s2現在為常量池中「kvill」的地址,所以有s2==s1.intern()為true。
5. 關於equals()和==:
這個對於String簡單來說就是比較兩字元串的Unicode序列是否相當,如果相等返回true;而==是比較兩字元串的地址是否相同,也就是是否是同一個字元串的引用。
6. 關於String是不可變的
這一說又要說很多,大家只要知道String的實例一旦生成就不會再改變了,比如說:String str=」kv」+」ill」+」 「+」ans」;
就是有4個字元串常量,首先」kv」和」ill」生成了」kvill」存在內存中,然後」kvill」又和」 「 生成 」kvill 「存在內存中,最後又和生成了」kvill ans」;並把這個字元串的地址賦給了str,就是因為String的「不可變」產生了很多臨時變數,這也就是為什麼建議用StringBuffer的原因了,因為StringBuffer是可改變的。
7. java中兩個字元串的內存地址相同
String s1 = new String("I am a student"); 這里 你聲明了一個引用 s1\x0d\x0a指向的是 new String("I am a student"); 這個字元串\x0d\x0aString s4 = s1; 這里 你又聲明一個引用 指向s1的引用 也就是new String("I am a student"); \x0d\x0aif(s1 == s4) {\x0d\x0aSystem.out.println("這兩個字元串的內存位置相同");\x0d\x0a}\x0d\x0a上面還是相等的\x0d\x0a但是 s4 = s4.replace('a', 'A');\x0d\x0as4.replace('a', 'A'); 生成了另一個 字元串 你要知道 String 是final類型的 所以\x0d\x0a這個時候 即原來已經開辟了內存空間的 new String("I am a student"); 是不可能改變內容了的\x0d\x0a這個時候 s4.replace('a', 'A'); 就另開辟了一個內存空間\x0d\x0a 而這個時候 你的S4指向s4.replace('a', 'A'); 而原來的s1還是指向 new String("I am a student"); \x0d\x0a他們肯定不一樣的呢