Java中的字符串也是一連串的字符但是與許多其他的計算機語言將字符串作為字符數組處理不同Java將字符串作為String類型對象來處理將字符串作為內置的對象處理允許Java提供十分豐富的功能特性以方便處理字符串
JVM運行時數據區的內存模型由五部分組成
()方法區
()堆
()JAVA棧
()PC寄存器
()本地方法棧
對於String s = haha 它的虛擬機指令
: ldc ; //String haha : astore_ : return
ldc指令格式
ldcindex
ldc指令過程要執行ldc指令JVM首先查找index所指定的常量池入口在index指向的JVM常量池入口JVM將會查找CONSTANT_Integer_infoCONSTANT_Float_info和CONSTANT_String_info入口如果還沒有這些入口JVM會解析它們而對於上面的hahaJVM會找到CONSTANT_String_info入口同時將把指向被拘留String對象(由解析該入口的進程產生)的引用壓入操作數棧
astore_指令格式
astore_
astore_指令過程要執行astore_指令JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值然後將該值存入由索引指定的局部變量中即將引用類型或者returnAddress類型值存入局部變量
return 指令的過程
從上面的ldc指令的執行過程可以得出s的值是來自被拘留String對象(由解析該入口的進程產生)的引用即可以理解為是從被拘留String對象的引用復制而來的故我個人的理解是s的值是存在棧當中上面是對於s值得分析接著是對於haha值的分析我們知道對於String s = haha 其中haha值在JAVA程序編譯期就確定下來了的簡單一點說就是haha的值在程序編譯成class文件後就在class文件中生成了(大家可以用UE編輯器或其它文本編輯工具在打開class文件後的字節碼文件中看到這個haha值)執行JAVA程序的過程中第一步是class文件生成然後被JVM裝載到內存執行那麼JVM裝載這個class到內存中其中的haha這個值在內存中是怎麼為其開辟空間並存儲在哪個區域中呢?
JVM常量池
虛擬機必須為每個被裝載的類型維護一個常量池常量池就是該類型所用到常量的一個有序集和包括直接常量(stringinteger和floating point常量)和對其他類型字段和方法的符號引用對於String常量它的值是在常量池中的而JVM常量池在內存當中是以表的形式存在的對於String類型有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值注意該表只存儲文字字符串值不存儲符號引用說到這裡對JVM常量池中的字符串值的存儲位置應該有一個比較明了的理解了
在介紹完JVM常量池的概念後接著談開始提到的haha的值的內存分布的位置對於haha的值實際上是在class文件被JVM裝載到內存當中並被引擎在解析ldc指令並執行ldc指令之前JVM就已經為haha這個字符串在常量池的CONSTANT_String_info表中分配了空間來存儲haha這個值
既然haha這個字符串常量存儲在常量池中常量池是屬於類型信息的一部分類型信息也就是每一個被轉載的類型這個類型反映到JVM內存模型中是對應存在於JVM內存模型的方法區中也就是這個類型信息中的JVM常量池概念是存在於在方法區中而方法區是在JVM內存模型中的堆中由JVM來分配的所以haha的值是應該是存在堆空間中的而對於String s = new String(haha) 它的JVM指令
: new ; //class String
: dup
: ldc ; //String haha
: invokespecial ; //Method java/lang/String:(Ljava/lang/String;)V
: astore_
: return
new指令格式new indexbyteindexbyte
new指令過程
要執行new指令Jvm通過計算(indextype<<)|indextype生成一個指向常量池的無符號位索引然後JVM根據計算出的索引查找JVM常量池入口該索引所指向的常量池入口必須為CONSTANT_Class_info如果該入口尚不存在那麼JVM將解析這個常量池入口該入口類型必須是類JVM從堆中為新對象映像分配足夠大的空間並將對象的實例變量設為默認值最後JVM將指向新對象的引用objectref壓入操作數棧
dup指令格式dup
dup指令過程
要執行dup指令JVM復制了操作數棧頂部一個字長的內容然後再將復制內容壓入棧本指令能夠從操作數棧頂部復制任何單位字長的值但絕對不要使用它來復制操作數棧頂部任何兩個字長(long型或double型)中的一個字長上面例中即復制引用objectref這時在操作數棧存在個引用
ldc指令格式ldcindex
ldc指令過程
要執行ldc指令JVM首先查找index所指定的常量池入口在index指向的JVM常量池入口JVM將會查找CONSTANT_Integer_infoCONSTANT_Float_info和CONSTANT_String_info入口如果還沒有這些入口JVM會解析它們而對於上面的hahaJVM會找到CONSTANT_String_info入口同時將把指向被拘留String對象(由解析該入口的進程產生)的引用壓入操作數棧
invokespecial指令格式invokespecialindextypeindextype
invokespecial指令過程對於該類而言該指令是用來進行實例初始化方法的調用上面例子中即通過其中一個引用調用String類的構造器初始化對象實例讓另一個相同的引用指向這個被初始化的對象實例然後前一個引用彈出操作數棧
astore_指令格式astore_
astore_指令過程
要執行astore_指令JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值然後將該值存入由索引指定的局部變量中即將引用類型或者returnAddress類型值存入局部變量
return 指令的過程:
從方法中返回返回值為void要執行astore_指令JVM從操作數棧頂部彈出一個引用類型或者returnAddress類型值然後將該值存入由索引指定的局部變量中即將引用類型或者returnAddress類型值存入局部變量
通過上面個指令可以看出String s = new String(haha);中的haha存儲在堆空間中而s則是在操作數棧中上面是對s和haha值的內存情況的分析和理解;那對於String s = new String(haha);語句到底創建了幾個對象呢?這裡haha本身就是JVM常量池中的一個對象而在運行時執行new String()時將JVM常量池中的對象復制一份放到堆中並且把堆中的這個對象的引用交給s持有所以這條語句就創建了個String對象下面是一些String相關的常見問題
String中的final用法和理解
final StringBuffer a = new StringBuffer(); final StringBuffer b = new StringBuffer(); a=b;//此句編譯不通過 final StringBuffer a = new StringBuffer(); aappend();//編譯通過
可見final只對引用的值(即內存地址)有效它迫使引用只能指向初始指向的那個對象改變它的指向會導致編譯期錯誤至於它所指向的對象的變化final是不負責的
String 常量池問題的幾個例子
下面是幾個常見例子的比較分析和理解
final StringBuffer a = new StringBuffer();
final StringBuffer b = new StringBuffer();
a=b;//此句編譯不通過
final StringBuffer a = new StringBuffer();
aappend();//編譯通過
分析JVM對於字符串常量的+號連接將程序編譯期JVM就將常量字符串的+連接優化為連接後的值拿a + 來說經編譯器優化後在class中就已經是a在編譯期其字符串常量的值就確定下來故上面程序最終的結果都為true
String a = ab;
String bb = b; String b = a + bb;
Systemoutprintln((a == b));
//result = false
分析JVM對於字符串引用由於在字符串的+連接中有字符串引用存在而引用的值在程序編譯期是無法確定的即a + bb無法被編譯器優化只有在程序運行期來動態分配並將連接後的新地址賦給b所以上面程序的結果也就為false
String a = ab;
final String bb = b;
String b = a + bb;
Systemoutprintln((a == b));
//result = true
分析和[]中唯一不同的是bb字符串加了final修飾對於final修飾的變量它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中所以此時的a + bb和a + b效果是一樣的故上面程序的結果為true
String a = ab; final String bb = getBB();
String b = a + bb;
Systemoutprintln((a == b));
//result = false private static String getBB() { return b; }
分析JVM對於字符串引用bb它的值在編譯期無法確定只有在程序運行期調用方法後將方法的返回值和a來動態連接並分配地址為b故上面程序的結果為false通過上面個例子可以得出得知
String s = a + b + c;
就等價於String s = abc;
String a = a; String b = b;
String c = c;
String s = a + b + c;
這個就不一樣了最終結果等於
StringBuffer temp = new StringBuffer();
tempappend(a)append(b)append(c);
String s = temptoString();
由上面的分析結果可就不難推斷出String 采用連接運算符(+)效率低下原因分析形如這樣的代碼
public class Test { public static void main(String args[]) {
String s = null;
for(int i = ; i < ; i++) { s += a; } } }
每做一次 + 就產生個StringBuilder對象然後append後就扔掉下次循環再到達時重新產生個StringBuilder對象然後 append 字符串如此循環直至結束 如果我們直接采用 StringBuilder 對象進行 append 的話我們可以節省 N 次創建和銷毀對象的時間所以對於在循環中要進行字符串連接的應用一般都是用StringBuffer或StringBulider對象來進行append操作String對象的intern方法理解和分析
public class Test {
private static String a = ab;
public static void main(String[] args){
String s = a;
String s = b;
String s = s + s;
Systemoutprintln(s == a);//false
Systemoutprintln(sintern() == a);//true
}
}
這裡用到Java裡面是一個常量池的問題對於s+s操作其實是在堆裡面重新創建了一個新的對象s保存的是這個新對象在堆空間的的內容所以s與a的值是不相等的而當調用sintern()方法卻可以返回s在JVM常量池中的地址值因為a的值存儲在常量池中故sintern和a的值相等
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25562.html