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

时间:2022-01-30 17:21:23

由常量池 运行时常量池 String intern方法想到的(三)之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引用时,会出现类型不匹配的问题),然后在栈中开辟一个空间用来存放这个文本字符串在运行时常量池中的地址。其内存模型如下所示: 
JVM-由常量池 运行时常量池 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。其内存模型如下所示: 
JVM-由常量池 运行时常量池 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文件的常量池中。 
其内存模型如下所示: 
JVM-由常量池 运行时常量池 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方法的源码如下: 
JVM-由常量池 运行时常量池 String intern方法想到的(三)之String内存模型 
其内存模型如下所示: 
JVM-由常量池 运行时常量池 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的引用。 
其内存模型如下所示: 
JVM-由常量池 运行时常量池 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返回。 
其内存模型如下所示: 
JVM-由常量池 运行时常量池 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
JVM-由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

由此可以看到java编译器进行了优化。

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

为此我专门做了一个小小地测试:
package start;

public class Test {
public static void main(String[] args) {
String s1 = new String("StringTest");
String s2 = new StringBuffer().append("wb").append("is").append("wb").toString();
String s3 = "ihaveaDream";
String s4 = new String("Monday") + "Friday";

System.out.println( s1.intern() == s1);
System.out.println( s2.intern() == s2);
System.out.println( s3.intern() == s3);
System.out.println( s4.intern() == s4);
/**
* 在jdk1.6中:常量池的内存是与堆内存物理隔离的,因为常量池的内存在永久代进行分配,永久代和Java堆的内存是物理隔离的。
* 输出结果为:
* false
* false
* true
* false
* 对于s1:new一个String,JVM会先去常量池中找“StringTest”,发现没有“StringTest”,这时候会在常量池中new一个“StringTest”,并且
* 会在堆内存复制一个在数据上与“StringTest”相同的对象,并返回它的堆内存引用s1。所以s1.intern()时,会先去常量池中找在数据上与“StringTest”
* 相同的对象,发现有,返回它,此时s1.intern() 指的就是常量池中的“StringTest”,而s1指的是堆内存中的“StringTest”,所以为false。
* 对于s2:先new一个StringBuffer对象,然后进行append()操作。根据javap -verbose Test.class反编译,可以看出在最后一次append()
* 操作后,此时常量池中有“wb”和“is”两个对象,最后进行一次toString()操作,会在堆内存中新建一个“wbiswb”对象,也就是s2的指向。所以s2.intern()时,
* 发现常量池中并 没有“wbiswb”对象,只有“wb”和“is”两个对象,所以JVM会在常量池中new一个“wbiswb”对象,然后将s2.intern()指向它,也就是说s2.int
* ern()指的是常量池中的“wbiswb”对象,s2指的是对内存中的“wbiswb”,明显不是同一个对象。
* 对于s3:jvm先去常量池中找“ihaveaDream”对象,发现没有,所以在常量池中new一个“ihaveaDream”对象,然后s3指向它。s3.intern()时,
* 先去常量池中找“ihaveaDream”对象,发现有,所以s3.intern()指的就是常量池中的“ihaveaDream”对象,此时 s3 == s3.intern();
* 对于s4:根据Test类文件的反编译结果可以看出,此时jvm会在常量池中new“Monday”和“Friday”两个对象,然后中间一个加号,就相当于在堆内存新建一个
* StringBuilder对象,StringBuilder对象根StringBuffer对象类似,最后会默认地调用StringBuilder的toString()方法在堆内存新建一个“MondayFriday”
* 对象,然后s4指向它。s4.intern()时,jvm先去常量池中找“MondayFriday”对象,没找到,会在常量池中new一个“MondayFriday”对象,然后s4.intern()指向它。
* 所以此时s4.intern()指向的是常量池中的“MondayFriday”对象,s4指的是堆内存中的“MondayFriday”对象。所以s4.intern() != s4;
*
* 在jdk1.7中:实现的常量池在java堆上分配内存
* 输出结果为:
* false
* true
* true
* true
*
* 对于s1:结合上面的分析JVM会在常量池和堆中分别new一个“StringTest”对象,然后s1指向堆中的对象,s1.intern()指向常量池中的对象, s1 != s1.intern();
* 对于s2:结合上面的分析JVM会在常量池中创建“wb”和“is”两个对象,在堆中创建“wbiswb”对象,s2指向堆中的“wbiswb”对象,s2.intern()发现此时常量池中并没有
* “wbiswb”对象(只有“wb”和“is”两个对象),然后根据jdk1.7会返回堆中的“wbiswb”对象给s2. s2 == s2.intern().
* 对于s3:跟jdk1.6的一样。 s3 == s3.intern().
* 对于s4:跟s2类似,s4和s4.intern()都是指向堆中的“MondayFriday”对象。 s4 == s4.intern().
*
* 总结:
* 1.对于String S = new String(String s),结果是堆和常量池中都会有 s对象,所以无论是在jdk1.6还是jdk1.7中, S != S.intern().
* 2.对于String S = "s",结果是S和S.intern()都指向常量池中的s对象。所以无论是在jdk1.6还是jdk1.7中, S == S.intern()。
* 3.对于s.intern()而言:在jdk1.6中永远指向常量池中的对象,没有就创建,然后指向;在jdk1.7中,常量池中有就指向常量池中的对象,没有却是指向堆中的对象。
*/
}
}