JVM常量池

在瞭解JVM常量池前,我們先來看看JVM。每個JVM都有兩種機制,一個是裝載具有合適名稱的類(類或是接口),叫做類裝載子系統;另外的一個負責執行包含在已裝載的類或接口中的指令,叫做運行引擎。每個JVM又包括方法區、堆、棧、程序計數器和本地方法棧這五個部分,這幾個部分和類裝載機制與運行引擎機制一起組成的體系結構圖爲:
 
JVM的每個實例都有一個它自己的方法域和一個堆,運行於JVM內的所有的線程都共享這些區域;當虛擬機裝載類文件的時候,它解析其中的二進制數據所包含的類信息,並把它們放到方法域中;當程序運行的時候,JVM把程序初始化的所有對象置於堆上;而每個線程創建的時候,都會擁有自己的程序計數器和 Java棧,其中程序計數器中的值指向下一條即將被執行的指令,線程的Java棧則存儲爲該線程調用Java方法的狀態;本地方法調用的狀態被存儲在本地方法棧,該方法棧依賴於具體的實現。
Java虛擬機的棧有三個區域:局部變量區、運行環境區、操作數區。
一個JVM實例只存在於一個堆類存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,以方便執行器執行,堆內存分爲三部分:
Permanent Space 永久存儲區
Young Generation Space 新生區
Tenure generation space養老區
永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的Class,Interface的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉JVM纔會釋放此區域所佔用的內存。
而新生區又分爲伊甸區(Eden space)和倖存者區(Survivor pace)。所有的類都是在伊甸區被new出來的。倖存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸區的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收,將伊甸園區中的不再被其他對象所引用的對象進行銷燬。然後將伊甸園中的剩餘對象移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,然後移動到倖存1區。那如果1區也滿了呢?再移動到養老區。
常量池是方法區的一部分。常量池(constant_pool)指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量和符號引用。運行時常量池是方法區的一部分。
在class文件結構中,最頭的4個字節用於存儲魔數Magic Number,用於確定一個文件是否能被JVM接受,再接着4個字節用於存儲版本號,前2個字節存儲次版本號,後2個存儲主版本號,再接着是用於存放常量的常量池,由於常量的數量是不固定的,所以常量池的入口放置一個2個字節無符號類型的數據(constant_pool_count)存儲常量池容量計數值。常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:
類和接口的全限定名
字段名稱和描述符
方法名稱和描述符
Java中八種基本類型的包裝類的大部分都實現了常量池技術,它們是Byte、Short、Integer、Long、Character、Boolean,另外兩種浮點數類型的包裝類(Float、Double)則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值在-128到127時纔可使用對象池。看下面代碼:
使用javap查看生成的字節碼:javap -verbose Test
minor version:次版本號  (minor:二流的)
major version:主版本號  (major:主修)
  bipush指令意思是將單字節的常量值(-128~127)推送到棧頂
sipush指令意思是將短型的常量值(-32768~32767)推送到棧頂
invokestatic指令意思是調用靜態方法。


其它封裝類如下:
Integer num1=10;
Integer num2=10;
Integer num3=128;
Integer num4=128;


System.out.println(num1==num2); //輸出true
System.out.println(num3==num4); //輸出false


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 
     
String s =  new  String( "xyz" );  在運行時涉及幾個String實例?
答案是:如果是第一次運行則應該是兩個,一個是字符串字面量"xyz"所對應的駐留(intern)在一個全局共享的字符串常量池中的實例,另一個是通過new String(String)創建並初始化的、內容與"xyz"相同的實例。


String中的final用法和理解:
final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。
final StringBuffer a = new StringBuffer("yc");
final StringBuffer b = new StringBuffer("ycInfo");
a=b;//此句編譯不通過 


final StringBuffer sbf = new StringBuffer("yc");
sbf.append("info");//編譯通過  


案例1:
String str1 = "yc1";
String str2 = "yc" + 1;
System.out.println( str1==str2 ); //true


String str3 = "ycinfo";
String str4 = "yc" + "info";
System.out.println( str3==str4 ); //true
String str5 = "yc3.14";
String str6 = "yc" + 3.14;
System.out.println( str5==str6 ); //true    
JVM對於字符串常量的"+"連接符在程序編譯期就會將常量字符串的"+"連接優化爲連接後的值,拿"yc" + 1來說,經編譯器優化後在class中就已經是yc1。在編譯期其字符串常量的值就確定下來,故上面程序最終的結果都爲true。


 案例2:
String str1 = "ycinfo";
String str2="yc";
String str3 = str2+"info";


System.out.println( str1==str3 ); //false   
JVM對於字符串引用,由於在字符串的"+"連接中,有字符串引用存在,而引用的值在程序編譯期是無法確定的,即str2+"info"  無法被編譯器優化,只有在程序運行期,來動態分配並將連接後的新地址賦給str3。所以上面程序的結果也就爲false。


案例3:
String str1 = "ycinfo";
final String str2="yc";
String str3 = str2+"info";


System.out.println( str1==str3 ); //true
和上面唯一不同的是"yc"字符串加了final修飾,對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。所以此時的str2 + "info"和"yc" + "info"效果是一樣的。故上面程序的結果爲true。


 案例4:
public static void main(String[] args) {
String str1 = "ycinfo";
final String info= getInfo();
String str2 = "yc" + info;
System.out.println( str1==str2 ); //false
}


private static String getInfo() {
return "info";
JVM對於字符串引用info,它的值在編譯期無法確定,只有在程序運行期調用方法後,將方法的返回值和"yc"來動態連接並分配地址爲str2,故上面程序的結果爲false。
 
通過上面4個例子可以得出得知:
String str1 = "yc" + "in" + "fo";  等價於
String str2 = "ycinfo";
int num1 = 1+2+3; 等價於
int num2 = 6;


由上面的分析結果,可就不難推斷出String 採用連接運算符(+)效率低下原因了,形如這樣的代碼:
public class Test {
    public static void main(String args[]) {
        String str = null;
        for(int i = 0; i < 100; i++) {
            str += "yc";
        }
    }
每做一次 + 就產生個StringBuilder對象,然後append後就扔掉。下次循環再到達時重新產生個StringBuilder對象,然後 append 字符串,如此循環直至結束。 如果我們直接採用 StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次創建和銷燬對象的時間。所以對於在循環中要進行字符串連接的應用,一般都是用StringBuffer或StringBulider對象來進行 append操作。
 
String.intern()分析
Java語言並不要求常量一定只能在編譯期產生,運行時也可能將新的常量放入常量池中,這種特性用的最多的就是String.intern()方法。
String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用。
String str1= "yc";
String str2=new String("yc");
String str3=new String("yc");


System.out.println( str1==str2 ); //false
str2.intern(); //intern:實習生  拘留 軟禁
str3=str3.intern(); //把常量池中"yc"的引用賦給str3
System.out.println( str1==str2 ); //false 雖然執行了str2.intern(),但它的返回值沒有賦給str2 
System.out.println( str1==str2.intern() ); //true 說明str2.intern()返回的是常量池中”yc”的引用 
System.out.println( str1==str3 ); //true 
有人說,使用String.intern()方法則可以將一個String類的保存到一個全局String表中,如果具有相同值的Unicode字符串已經在這個表中,那麼該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的地址註冊到表中。如果我把他說的這個全局的String 表理解爲常量池的話,他的最後一句話:如果在表中沒有相同值的字符串,則將自己的地址註冊到表中是錯的:
String str1=new String("ycinfo");
String str2=str1.intern();
System.out.println( str1==str1.intern() ); //false
System.out.println(str1==str2); //fals
System.out.println( str2==str1.intern() ); //true
來源:http://chenzehe.iteye.com/blog/1727062

發佈了70 篇原創文章 · 獲贊 78 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章