由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

时间:2021-07-27 17:06:36

前面的文章由常量池 运行时常量池 String intern方法想到的(二) 说了一些关于java字节码的东西,这篇博文围绕String做一些总结。

注意

在这篇博文中描述的,所有在运行时常量池中出现的字符串其实都是一个String对象。因为,java是一种强类型的语言,要求每一种变量都要有具体的数据类型。但是基本数据类型存放的不是对象(String不属于基本数据类型)。基本数据类型的常量在运行时常量池中存放的是字面值。貌似JVM会自动将boolean、byte、char、short自动转换成int型。(有待确认)。 那如何区分int long float double呢。整形和浮点型很容易区分,int和float只占一个slot,long和double要占两个slot。

声明

本文讨论的内容都是基于JDK1.6。

java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)

String的内存布局

  • s = “12”
public class Test {
public static void main(String[] args) {
String s = "12";
}
}

上面的代码会发生什么?
看下上面代码的字节码指令:

Compiled from "Test.java"
public class Test extends java.lang.Object
SourceFile: "Test.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #4.#13; // java/lang/Object."<init>":()V
const #2 = String #14; // 12
const #3 = class #15; // Test
const #4 = class #16; // java/lang/Object
const #5 = Asciz <init>;
const #6 = Asciz ()V;
const #7 = Asciz Code;
const #8 = Asciz LineNumberTable;
const #9 = Asciz main;
const #10 = Asciz ([Ljava/lang/String;)V;
const #11 = Asciz SourceFile;
const #12 = Asciz Test.java;
const #13 = NameAndType #5:#6;// "<init>":()V
const #14 = Asciz 12;
const #15 = Asciz Test;
const #16 = Asciz java/lang/Object;

{
public Test();
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[]);
Code:
Stack=1, Locals=2, Args_size=1
0: ldc #2; //String 12
2: astore_1
3: return
LineNumberTable:
line 3: 0
line 4: 3


}

main方法的字节码指令只有2条

   0:  ldc #2; //String 12
2: astore_1

将运行时常量池中的常量”12”压入栈中,然后将这个栈中的”12”存入局部量量表的slot1中(注意,slot0中存放的是this)。
当javac去编译Test.java时,发现了文本字符串”12”,会将这个”12”放入class文件的常量池中,当class文件被加载到JVM时,会将class文件中的常量池存放在运行时常量池中(这个时候应该是在运行时常量池中new出了一个String对象,如果只是存放字符串,在返回给s引用时,会出现类型不匹配的问题),然后在栈中开辟一个空间用来存放这个文本字符串在运行时常量池中的地址。其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

  • s = new String(“12”)
public class Test {
public static void main(String[] args) {
String s = new String("12");
}
}

其字节码指令如下:

public static void main(java.lang.String[]);
Code:
Stack=3, Locals=2, Args_size=1
0: new #2;
//class java/lang/String
3: dup
4: ldc #3;
//String 12
6: invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return

同样javac会将”12”放入class文件的常量池中,在类加载时存入运行时常量池。从字节码指令上来看,JVM会先在堆上new出一块内存,用来存放String对象,这个时候这个String对象中还没有进行init,也就没有内容,当调用init之后,通过astore_1将堆中的String对象的地址赋值给局部变量s。其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型
其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • s = “12” + “3”
public class Test {
public static void main(String[] args) {
String s = "12" + "3";
}
}

其对应的java字节码指令如下:

Compiled from "Test.java"
public class Test extends java.lang.Object
SourceFile: "Test.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #4.#13; // java/lang/Object."<init>":()V
const #2 = String #14; // 123
const #3 = class #15; // Test
const #4 = class #16; // java/lang/Object
const #5 = Asciz <init>;
const #6 = Asciz ()V;
const #7 = Asciz Code;
const #8 = Asciz LineNumberTable;
const #9 = Asciz main;
const #10 = Asciz ([Ljava/lang/String;)V;
const #11 = Asciz SourceFile;
const #12 = Asciz Test.java;
const #13 = NameAndType #5:#6;// "<init>":()V
const #14 = Asciz 123;
const #15 = Asciz Test;
const #16 = Asciz java/lang/Object;

{
public Test();
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[]);
Code:
Stack=1, Locals=2, Args_size=1
0: ldc #2; //String 123
2: astore_1
3: return
LineNumberTable:
line 3: 0
line 4: 3


}

这个是不是很简单!java编译器在编译阶段就完成了优化,将文本字符串”12”和”3”,在编译时就拼接成了”123“存放在了class文件的常量池中。
其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

  • s = new String(“12”) + new String(“3”)

下面再看看上篇博文提出的问题是什么样的。

public class Test {
public static void main(String[] args) {
String s = new String("12") + new String("3");
}
}

其java字节码如下所示:

public static void main(java.lang.String[]);
Code:
Stack=4, Locals=2, Args_size=1
0: new #2;
//class java/lang/StringBuilder
3: dup
4: invokespecial #3;
//Method java/lang/StringBuilder."<init>":()V
7: new #4; //class java/lang/String
10: dup
11: ldc #5;
//String 12
13: invokespecial #6; //Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4; //class java/lang/String
22: dup
23: ldc #8;
//String 3
25: invokespecial #6; //Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return

这句话产生的字节码明显多了。主要原因是,javac会对String的+操作符进行优化,使用StringBuilder的append方法实现。
javac首先会将文本字符串放在class文件的常量池中,当类加载时存放在运行时常量池中。从字节码指令来看,首先在堆中new的是StringBuilder对象,然后new出了String对象,将”12“复制到String对象中,使用StringBuilder的append方法拼接,然后再new出一个String对象,将”3“复制到String对象中,使用append方法拼接,最后调用StringBuilder的toString(从StringBuilder#toString方法的源码可以看出,toString方法会new一个String对象)方法返回一个String引用。StringBuilder的toString方法的源码如下:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型
其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型
其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • s = “12” + new String(“3”)
public class Test {
public static void main(String[] args) {
String s = "12" + new String("3");
}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);
Code:
Stack=4, Locals=2, Args_size=1
0: new #2;
//class java/lang/StringBuilder
3: dup
4: invokespecial #3;
//Method java/lang/StringBuilder."<init>":()V
7: ldc #4; //String 12
9: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: new #6; //class java/lang/String
15: dup
16: ldc #7;
//String 3
18: invokespecial #8; //Method java/lang/String."<init>":(Ljava/lang/String;)V
21: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: return

这个就无法在编译器进行优化了。java编译器还是会将”12”和”3”放在class文件的常量池中,在类加载时放入运行时常量池中。在执行时会new一个StringBuilder对象,将”12”压入栈中,使用append方法进行连接,然后在堆上new一个String对象用来存放”3”,然后使用append方法进行连接,最后调用StringBuilder的toString(会new一个String对象)方法返回一个String的引用。
其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型
其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • String t = “12”; String s = t + “3”;
public class Test {
public static void main(String[] args) {
String t = "12";
String s = t + "3";
}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: ldc #2;
//String 12
2: astore_1
3: new #3;
//class java/lang/StringBuilder
6: dup
7: invokespecial #4;
//Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5;
//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6; //String 3
16: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return

虽然 t 是一个String,但是在编译器无法对其进行优化。从上面的字节码中看到这个的Locals=3,因为有两个局部变量 t 和 s。
java编译器将”12”和”3”放入class文件常量池中,在运行时加载到运行时常量池中。其中”12”会存入到局部变量表的slot1(t 局部变量中),在堆中new一个StringBuilder对象,使用append方法连接”12”,然后调用append方法连接”3”,最后调用StringBuilder#toString返回。
其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型
其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • final String t = “12”; String s = t + “3”;
public class Test {
public static void main(String[] args) {
final String t = "12";
String s = t + "3";
}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);
Code:
Stack=1, Locals=3, Args_size=1
0: ldc #2; //String 123
2: astore_2
3: return

由此可以看到java编译器进行了优化。当是final型变量时不会当成变量操作,而是在编译器就进行了替换。注意其Locals=3。
其内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

结束语

这篇博文主要介绍了一下String对象在各种情况下赋值时在内存中的模型,下一篇博文主要介绍下String#intern方法的内部实现及各种案例的分析。

参考资料:
http://blog.csdn.net/rainnnbow/article/details/50461303#java