java 字符串內存分配的分析與總結


經常在網上各大版塊都能看到對於java字符串運行時內存分配的探討,形如:String a = "123",String b = new String("123"),這兩種形式的字符串是存放在什么地方的呢,其實這兩種形式的字符串字面值"123"本身在運行時既不是存放在棧上,也不是存放在堆上,他們是存放在方法區中的某個常量區,並且對於相同的字符串字面值在內存中只保留一份。下面我們將以實例來分析。

1.==運算符作用在兩個字符串引用比較的兩個案例:

public class StringTest { public static void main(String[] args) { //part 1
        String s1 = "i love china"; String s2 = "i love china"; System.out.println("result:" + s1 == s2);//程序運行結果為true //part 2
        String s3 = new String("i love china"); String s4 = new String("i love china"); System.out.println("result:" + s3 == s4);//程序運行結果為false } }

我們知道java中==運算符比較的是變量的值,對於引用類型對應的變量的值存放的是引用對象的地址,在這里String是引用類型,這里面的四個變量的值存放的其實是指向字符串的地址。對於part2的執行結果是顯然的,因為new操作符會使jvm在運行時在堆中創建新的對象,兩個不同的對象的地址是不同的。但是由part1的執行結果,可以看出s1和s2是指向的同一個地址,那么由變量s1,s2指向的字符串是存放在什么地方的呢,jvm又是對字符串如何處理的呢。同樣的對於變量s3,s4所指向的堆中的不同的字符串對象,他們會分別在自己的對象空間中保存一份"i love china"字符串嗎,為了了解jvm是如何處理字符串,首先我們看java編譯器生成的字節碼指令。通過字節碼指令我們來分析jvm將會執行哪些操作。

2.以下為程序生成的部分字節碼信息。紅色標注的是我們需要關注的部分。

 

Constant pool: #1 = Class              #2             // StringTest
   #2 = Utf8 StringTest #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object #5 = Utf8               <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8               this #13 = Utf8 LStringTest; #14 = Utf8 main #15 = Utf8               ([Ljava/lang/String;)V  #16 = String #17 // i love china 字符串地址的引用 #17 = Utf8 i love china #18 = Fieldref           #19.#21        // java/lang/System.out:Ljava/io/PrintStream;
  #19 = Class              #20            // java/lang/System
  #20 = Utf8               java/lang/System #21 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #22 = Utf8 out #23 = Utf8               Ljava/io/PrintStream; #24 = Class              #25            // java/lang/StringBuilder
  #25 = Utf8               java/lang/StringBuilder #26 = String             #27            // result:
  #27 = Utf8 result:

#28 = Methodref #24.#29 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#29 = NameAndType #5:#30 // "<init>":(Ljava/lang/String;)V
#30 = Utf8 (Ljava/lang/String;)V
#31 = Methodref #24.#32 // java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
#32 = NameAndType #33:#34 // append:(Z)Ljava/lang/StringBuilder;
#33 = Utf8 append
#34 = Utf8 (Z)Ljava/lang/StringBuilder;
#35 = Methodref #24.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#36 = NameAndType #37:#38 // toString:()Ljava/lang/String;
#37 = Utf8 toString
#38 = Utf8 ()Ljava/lang/String;
#39 = Methodref #40.#42 // java/io/PrintStream.println:(Ljava/lang/String;)V
#40 = Class #41 // java/io/PrintStream
#41 = Utf8 java/io/PrintStream
#42 = NameAndType #43:#30 // println:(Ljava/lang/String;)V
#43 = Utf8 println
#44 = Class #45 // java/lang/String
#45 = Utf8 java/lang/String
#46 = Methodref #44.#29 // java/lang/String."<init>":(Ljava/lang/String;)V
#47 = Utf8 args
#48 = Utf8 [Ljava/lang/String;
#49 = Utf8 s1
#50 = Utf8 Ljava/lang/String;
#51 = Utf8 s2
#52 = Utf8 s3
#53 = Utf8 s4
#54 = Utf8 StackMapTable
#55 = Class #48 // "[Ljava/lang/String;"
#56 = Utf8 SourceFile
#57 = Utf8 StringTest.java

 ........... //對應的方法的字節碼指令,由jvm運行時解釋執行。 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=5, args_size=1
         0: ldc #16       // String i love china,該指令是將常量池的#16處符號引用,在這里為字符串“ilove china”符號引用push到棧頂。該指令與底下的指令2對應於程序中的String s1 = "i love china"語句
        2: astore_1  //將棧頂的對象引用賦值給局部變量1. 3: ldc #16         // String i love china,同0處的指令,指向的是同一個符號引用處的常量。該指令與底下的指令5對應於程序中的 String s2 = "i love china"語句。
         5: astore_2  //將棧頂的對象引用賦值給局部變量2. 6: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
         9: new           #24                 // class java/lang/StringBuilder
        12: dup 13: ldc           #26                 // String result:
        15: invokespecial #28                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        18: aload_1 19: aload_2 20: if_acmpne 27  //彈出棧頂兩個對象引用進行比較其是否相等,不等,轉到指令27處,執行,相等執行下一條指令
        23: iconst_1 24: goto          28
        27: iconst_0 28: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
        31: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: invokevirtual #39                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: new #44          // class java/lang/String,創建一個對象,該對象位於常量池#44引用處,這里為String對象,並將對象引用push到棧頂。
        40: dup //拷貝棧頂一份對象引用push到棧頂。 41: ldc #16 // String i love china,同0,3處指令。
        43: invokespecial #46       // Method java/lang/String."<init>":(Ljava/lang/String;)V
        46: astore_3         47: new #44                 // class java/lang/String//創建一個對象,並將對象引用push到棧頂
        50: dup 51: ldc #16               // String i love china, 將字符串的符號引用push到棧頂。
        53: invokespecial #46   // Method java/lang/String."<init>":(Ljava/lang/String;)V,根據棧頂的對應的對象引用及字符串引用調用對象的init初始化方法,對字符串對象初始化
        56: astore 4       //將棧頂對象引用賦值給變量4.
        58: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
        61: new           #24                 // class java/lang/StringBuilder
        64: dup 65: ldc           #26                 // String result:
        67: invokespecial #28                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        70: aload_3 71: aload         4
        73: if_acmpne     80
        76: iconst_1 77: goto          81
        80: iconst_0 81: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
        84: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        87: invokevirtual #39                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        90: return
.........

LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 11: 37
line 12: 47
line 13: 58
line 14: 90
LocalVariableTable:
Start Length Slot Name Signature
0 91 0 args [Ljava/lang/String;//局部變量0
3 88 1 s1 Ljava/lang/String;  //局部變量1
6 85 2 s2 Ljava/lang/String;//局部變量2
47 44 3 s3 Ljava/lang/String;//局部變量3
58 33 4 s4 Ljava/lang/String;//局部變量4

 
        

字節碼中紅色的部分是與我們討論相關的。通過生成的字節碼,我們可以對示例程序得出如下結論。

1).java編譯器在將程序編譯成字節碼的過程中,對遇到的字符串常量"i love china"首先判斷其是否在字節碼常量池中存在,不存在創建一份,存在的話則不創建,也就是相等的字符串,只保留一份,通過符號引用可以找到它,這樣使得程序中的字符串變量s1和s2都是指向常量池中的同一個字符串常量。在運行時jvm會將字節碼常量池中的字符串常量存放在方法區中的通常稱之為常量池的位置,並且字符串是以字符數組的形式通過索引來訪問的。jvm在運行時將s1與s2指向的字符串相對引用地址指向字符串實際的內存地址。

2).對於String s3 = new String("i love china"),String s4 = new String("i love china"),由字節碼可以看出其是調用了new指令,jvm會在運行時創建兩個不同的對象,s3與s4指向的是不同的對象地址。所以s3==s4比較的結果為false。

其次,對於s3與s4對象的初始化,從字節碼看出是調用對象的init方法並且傳遞的是常量池中”i love china”的引用,那么創建String對象及初始化究竟干了什么,我們可以查看通過查看String的源碼及String對象生成的字節碼,以便更好的了解對於new String("i love china")時,在對象內部是做了字符串的拷貝還是直接指向該字符串對應的常量池的地址的引用。

 

3.String對象的部分源碼:

 

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    public String() {
        this.value = new char[0];
    }
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

 

  從源碼中我們看到String類里有個實例變量 char value[],通過構造方法我們可知,對象在初始化時並沒有做拷貝操作,只是將傳遞進來的字符串對象的地址引用賦給了實例變量value。由此我們可以初步的得出結論:即使使用new String("abc")創建了一個字符串對象時,在內存堆中為該對象分配了空間,但是在堆上並沒有存儲"abc"本身的任何信息,只是初始化了其內部的實例變量到"abc"字符串的引用。其實這樣做也是為了節省內存的存儲空間,以及提高程序的性能。

 

4.下面我們來看看String對象部分字節碼信息: 

  public java.lang.String(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1
         0: aload_0 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0 5: iconst_0 6: newarray       char
         8: putfield      #2                  // Field value:[C
        11: return LineNumberTable: line 137: 0 line 138: 4 line 139: 11

  public java.lang.String(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2
         0: aload_0 //將局部變量0push到棧頂,自身對象的引用。 1: invokespecial #1                  // Method java/lang/Object."<init>":()V 彈出棧頂對象引用調用該對象的#1處的初始化方法。
         4: aload_0 //將自身對象引用push到棧頂。 5: aload_1 //傳遞的字符串引用push到棧頂。 6: getfield #2                 // Field value:[C // 彈出棧頂的字符串引用並將其賦值給#2處的實例變量,並將其存放到棧上。
         9: putfield #2                 // Field value:[C //  彈出棧頂的字符串引用及對象自身的引用並將字符串的引用賦值給本對象自身的實例變量。
        12: aload_0 13: aload_1 14: getfield      #3                  // Field hash:I
        17: putfield      #3                  // Field hash:I
        20: return

 從字節碼的角度我們可以得出結論,new String("abc")在構造新對象時執行的是字符串引用的賦值,而不是字符串的拷貝。以上是從源碼及字節碼的角度來對字符串的內存分配進行的分析與總結。

 

 


免責聲明!

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



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