关于内存分区参见:
Java内存详解 - 内存分区
本文重点描述String对象创建时的内存分配方式。
字符串常量池概述
Java运行时会维护一个String� Pool,也叫“字符串缓存区”。 String Pool用来保存运行时产生的各种字符串,并且池中的字符串内容不重复。
String Pool的存在主要为了同样的字符串内容只保存一份,同样内容的字符串创建新对象时,只需要创建一个新的引用即可。
需要注意的是:jdk1.7 之前 hotspot JVM中常量池位于方法区,而jdk1.7之后,常量池从方法区移除,移到了堆中。
创建String对象的方式
String a = "test";
public class ReferenceTest {
public static void main(String[] args){
String a ="test";
}
}
用javap -v 反编译
public class ReferenceTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // test
#3 = Class #15 // ReferenceTest
#4 = Class #16 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 ReferenceTest.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 test //常量池中保存了test字符串
#15 = Utf8 ReferenceTest
#16 = Utf8 java/lang/Object
{
public ReferenceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // 加载常量"text"到操作数栈
2: astore_1 // 将操作数栈顶数值存储到局部变量表中的变量
3: return
LineNumberTable:
line 5: 0
line 7: 3
}
说明
只在常量池中新建了“test”字符串,没有在堆中新建对象,因为没有字节码指令:new
String b = new String("test")
public class ReferenceTest {
public static void main(String[] args){
String a = "test";
javap -v ReferenceTest.class
public class ReferenceTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = String #16 // test
#3 = Class #17 // java/lang/String
#4 = Methodref #3.#18 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #19 // ReferenceTest
#6 = Class #20 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 ReferenceTest.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 test
#17 = Utf8 java/lang/String
#18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V
#19 = Utf8 ReferenceTest
#20 = Utf8 java/lang/Object
#21 = Utf8 (Ljava/lang/String;)V
{
public ReferenceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // 加载常量"text"到操作数栈
2: astore_1 // 将操作数栈顶数值存储到局部变量表中的变量a
3: new #3 // 新建String类实例,即在堆内为该实例分配内存空间,并将地址压入操作数栈顶;
6: dup //复制操作数栈顶值,并将其压入栈顶(也就是说目前操作数栈上有两个相同的指向堆的地址)
7: ldc #2 // 加载常量池内字符串"test"到操作数栈顶
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 调用String实例的初始化方法new String(String input), 弹出操作数栈顶的2个值,分别为常量字符串值“test”,和一个新实例对应的堆地址,进行赋值,将堆地址指向常量字符串地址
12: astore_2 //将操作数栈顶数值(堆上新实例的内存地址)存储到局部变量表中的变量b
13: return
LineNumberTable:
line 5: 0
line 6: 3
line 8: 13
}
说明
因为“test”字符串在常量池中只存在一份,所以new String("test")这里只在堆上新建一个对象,引用常量池中己有的字符串值。
那么如果这个字符串之前在常量池中不存在,又将如何分配内存呢?我们看下一个例子:
String b = new String("test1")
public class ReferenceTest {
public static void main(String[] args){
String a = "test";
String b = new String("test1");
}
}
javap -v ReferenceTest.class
public class ReferenceTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#16 // java/lang/Object."<init>":()V
#2 = String #17 // test
#3 = Class #18 // java/lang/String
#4 = String #19 // test1
#5 = Methodref #3.#20 // java/lang/String."<init>":(Ljava/lang/String;)V
#6 = Class #21 // ReferenceTest
#7 = Class #22 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 ReferenceTest.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = Utf8 test
#18 = Utf8 java/lang/String
#19 = Utf8 test1
#20 = NameAndType #8:#23 // "<init>":(Ljava/lang/String;)V
#21 = Utf8 ReferenceTest
#22 = Utf8 java/lang/Object
#23 = Utf8 (Ljava/lang/String;)V
{
public ReferenceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String test
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #4 // String test1
9: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: return
LineNumberTable:
line 5: 0
line 6: 3
line 8: 13
}
说明
此例与上例不同之处仅在于常量池中分别保存了"test"和"test1";
说明在new String("test1")操作时,jvm先去常量池中找"test1"是否存在,如果不存在,则先在常量池中创建,再在堆中创建对象,引用该常量池中的字符串,并返回堆中新对象的内存地址。
String c = new String("test" ) + b;
public class ReferenceTest {
public static void main(String[] args){
String b = new String("test");
String c = new String("test" ) + b;
}
}
javap -v ReferenceTest.class
Constant pool:
#1 = Methodref #10.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/lang/String
#3 = String #21 // test
#4 = Methodref #2.#22 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #23 // java/lang/StringBuilder
#6 = Methodref #5.#19 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#24 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#25 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #26 // ReferenceTest
#10 = Class #27 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 SourceFile
#18 = Utf8 ReferenceTest.java
#19 = NameAndType #11:#12 // "<init>":()V
#20 = Utf8 java/lang/String
#21 = Utf8 test
#22 = NameAndType #11:#28 // "<init>":(Ljava/lang/String;)V
#23 = Utf8 java/lang/StringBuilder
#24 = NameAndType #29:#30 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#25 = NameAndType #31:#32 // toString:()Ljava/lang/String;
#26 = Utf8 ReferenceTest
#27 = Utf8 java/lang/Object
#28 = Utf8 (Ljava/lang/String;)V
#29 = Utf8 append
#30 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#31 = Utf8 toString
#32 = Utf8 ()Ljava/lang/String;
{
public ReferenceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: new #2 // 新建String类实例,即在堆内为该实例分配内存空间,并将地址压入操作数栈顶;
3: dup //复制操作数栈顶值,并将其压入栈顶(也就是说目前操作数栈上有两个相同的指向堆的地址)
4: ldc #3 //加载常量池中"test"到操作数栈顶
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V //弹出操作数栈顶两个数值,调用new String(String input)进行初始化
9: astore_1 //将堆上新对象的地址赋值给变量1
10: new #5 //新建StringBuilder类实例,即在堆内为该实例分配内存空间,并将地址压入操作数栈顶;
13: dup //复制操作数栈顶值,并将其压入栈顶(也就是说目前操作数栈上有两个相同的指向堆的地址)
14: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V //弹出操作数栈顶值(StringBuilder新对象在内存上的地址,调用new StringBuilder()进行实例初始化)
17: new #2 // 新建String类实例,即在堆内为该实例分配内存空间,并将地址压入操作数栈顶;
20: dup //复制操作数栈顶值,并将其压入栈顶(也就是说目前操作数栈上有两个相同的指向堆内新String对象的地址)
21: ldc #3 //加载常量池中"test"到操作数栈顶
23: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 弹出操作数栈顶两个数值,调用new String(String input)进行初始化
26: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //调用StringBuilder.append(String input)方法,将String拼接到StringBuilder对象上
29: aload_1 //加载变量b的值
30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; /调用StringBuilder.append(String input)方法,将变量b的值拼接到StringBuilder对象上
33: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 调用StringBuilder的toString()方法
36: astore_2 //将操作数栈顶的String赋值给变量c
37: return
LineNumberTable:
line 6: 0
line 9: 10
line 11: 37
}
说明
- “test”字符串在常量池中只有一份
- String拼接(+)操作,jvm实际上转换成StringBuilder.append方法执行。
- 内存分配上,一共保存了1个常量池中的对象,和3个堆上的对象
- 拼接后形成的“testtest”并没有保存在常量池中
String d = c + b;
public class ReferenceTest {
public static void main(String[] args){
String b = new String("test");
String c = new String("test1" ) + b;
String d = c + b; //本例主要说明String变量拼接操作时,jvm的处理流程
}
}
javap -v ReferenceTest.class```
public class ReferenceTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/String
#3 = String #22 // test
#4 = Methodref #2.#23 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #24 // java/lang/StringBuilder
#6 = Methodref #5.#20 // java/lang/StringBuilder."<init>":()V
#7 = String #25 // test1
#8 = Methodref #5.#26 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #5.#27 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Class #28 // ReferenceTest
#11 = Class #29 // java/lang/Object
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 ReferenceTest.java
#20 = NameAndType #12:#13 // "<init>":()V
#21 = Utf8 java/lang/String
#22 = Utf8 test
#23 = NameAndType #12:#30 // "<init>":(Ljava/lang/String;)V
#24 = Utf8 java/lang/StringBuilder
#25 = Utf8 test1
#26 = NameAndType #31:#32 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#27 = NameAndType #33:#34 // toString:()Ljava/lang/String;
#28 = Utf8 ReferenceTest
#29 = Utf8 java/lang/Object
#30 = Utf8 (Ljava/lang/String;)V
#31 = Utf8 append
#32 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 toString
#34 = Utf8 ()Ljava/lang/String;
{
public ReferenceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String test
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #5 // class java/lang/StringBuilder
13: dup
14: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
17: new #2 // class java/lang/String
20: dup
21: ldc #7 // String test1
23: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: aload_1
30: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_2
37: new #5 // class java/lang/StringBuilder
40: dup
41: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
44: aload_2 // 加载变量c
45: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
48: aload_1 //加载变量b
49: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
52: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
55: astore_3 //赋值变量d
56: return
LineNumberTable:
line 6: 0
line 9: 10
line 11: 37
line 12: 56
}
说明
本例与上例的不同主要有两处:
- 常量池中分别保存"test"和"test1"
- String变量相加操作实际上就是分别加载两个变量值,进行StringBuilder.append()操作。
- 内存分配上,一共保存了2个常量池中的对象,和3个堆上的对象
- 拼接后形成的“test1test”并没有保存在常量池中
参考文档:
https://www.cnblogs.com/xiohao/p/4296088.html
https://www.cnblogs.com/ysocean/p/8571426.html
https://www.cnblogs.com/vinozly/p/5399308.html
网友评论