從JVM的角度解析String


1. 字符串生成過程

我們都知道String s = "hello java";會將“hello java”放入字符串常量池,但是從jvm的角度來看字符串和三個常量池有關,class常量池,運行時常量池,全局字符串常量池(也就是常說的字符串常量池)

第一個是class的常量池,看一下下面這個代碼

public class StringTest {
    public void test1() {
        String s = "hello java";
    }
}

 

如果用javap -v StringTest.class 來查看他的字節碼文件,代碼如下

Constant pool:
    #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
    #2 = String             #18            // hello java
	...
	#18 = Utf8               hello java

#2表示有一個字符串的索引指向#18,一個utf8編碼的字符串字面量,這個#18只代表由utf編碼的數據,不是java對象,#2是java對象,但是他現在還沒有初始化,#18是在文件編譯后就生成的

utf8字面量字符串,他在項目啟動加載類時就進入了運行時常量池。

那么就有一個問題,#2什么時候初始化,以及什么時候進入全局字符串常量池(也就是平常說的字符串常量池)呢?我們繼續看字節碼

源碼:
public void test1() {
    String s = "hello java";
}



字節碼:
public void test1();
         0: ldc           #2                  // String hello java
         2: astore_1
         3: return
為了方便看,去掉部分代碼
我們看字節碼第一行,很明顯的看到他調用了#2,ldc的作用是將常量池中的數據加載到操作數棧中(簡單來說就是進行數據操作的地方),這個時候#2肯定要初始化生成java對象了。
如果是java7及以后的版本這時候jvm會在堆中創建一個“hello java”的對象,。然后將這個對象的引用放入全局字符串變量池(也是在堆中)中,當以后出現“hello java”,能在全局字符串變量池找到,就不會再生成對象。
如果是java6版本jvm會在permGen中創建一個“hello java”的對象,。然后將這個對象的引用放入全局字符串變量池(在permGen)中,當以后出現“hello java”,能在全局字符串變量池找到,就不會再生成對象。

 

所以全局字符串常量池中存放的只是索引,他類似於java中的HashMap,key是字面量字符串,value是是指向真正字符串對象的引用。

2.String.intern

JDK1.6中,intern()方法會把首次遇到的字符串實例復制到永久代中然后把這個字符串的引用放入全局字符串常量池,返回的也是永久代中這個字符串的實例的引用。

JDK1.7中,intern()方法首次遇到字符串實例時不會在復制實例,直接把這個實例的引用存入全局字符串常量池,返回的就是這個字符串實例的引用。

那就有一個疑問,為什么jdk1.6要重新復制一份呢? 因為1.6時字符串常量池在永久代,而通過new 產生的字符串在堆中,兩個區域內存隔離,永久代無法存堆中的引用,

1.7時代,jvm把字符串常量池移到了堆中,所以在1.7中就不用創造實例了。

/**
 * jdk 1.8
 */
public static void main(String[] args) {
    //調用了new String
 String s1 = new StringBuilder("lh").append("cy").toString();
    String s2 = s1.intern();
    //true
 System.out.println(s1 == s2);
}

 

3.字符串相加

3.1編譯時確定的字符串相加

//源代碼
public static void main(String[] args) {
    String s = "lh" + "cy";
}
//字節碼類的常量池
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // lhcy
   #21 = Utf8               lhcy
//字節碼操作指令
0: ldc           #2                  // String lhcy
2: astore_1
3: return

可以看見 “lh”和“cy”被拼成了“lhcy”,這個結論大家應該早就知道,這里從字節碼角度來看一下。

3.2運行時確定的字符串相加

源代碼:
public String test(String s1, String s2) {
 return s1 + s2;
}


字節碼:
//新建一個StringBuilder對象
0: new           #2                  // class java/lang/StringBuilder  
//復制新建的StringBuilder對象
3: dup		
//消耗剛才復制的StringBuilder對象用於初始化	              
4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
//加載s1
7: aload_1
//拼接到StringBuilder
8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
加載s2
11: aload_2
//拼接到StringBuilder
12: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
//調用StringBuilder的toString方法
15: invokevirtual #5                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn

根據注釋應該知道了上述代碼創建了一個StringBuilder然后append,最后toString。那么又有一個小疑問,既然兩個字符串相加生成了StringBuilder,那么我們還

手動創建StringBuilder干嘛,為什么不讓我們之間使用“+”來拼接字符串。那么請看下面的代碼

//源代碼
public static void main(String[] args) {
    String s = "";
    for (int i = 0; i < 100; i++) {
        s += i;
    }
}
//jvm實際執行的代碼
public static void main(String[] args) {
    String s = "";
    for (int i = 0; i < 100; i++) {
        StringBuilder stringBuilder = new StringBuilder();
        s = stringBuilder.append(s).append(i).toString();
    }
}

從代碼塊中,我們發現當每次要給字符串賦值時,StringBuilder就會調用toString來新建字符串,jvm並不知道你只需要循環后的結果,在其中創建了大量無用的String對象,不僅耗時

創建了對象,並且占用了大量內存,從而加快了gc的頻率,對系統運行非常不利。

 

總結

String是一個常用的類,基本使用非常簡單,但是他的底層實現非常復雜,c++基礎不錯的同學可以去看一下String.intern()的源碼和ldc的源碼。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM