经常在网上各大版块都能看到对于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")在构造新对象时执行的是字符串引用的赋值,而不是字符串的拷贝。以上是从源码及字节码的角度来对字符串的内存分配进行的分析与总结。