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/String:()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 s          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/String:()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/String:()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")在构造新对象时执⾏的是字符串引⽤的赋值,⽽不是字符串的
拷贝。以上是从源码及字节码的⾓度来对字符串的内存分配进⾏的分析与总结。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。