【参考链接】
一个String对象在堆上占用几个字节
虽然String对象所代表的字符串可能是多种多样的,但是一个String对象在堆内存中所占用的内存空间大小是固定的,这是因为
为了节省内存空间,一个应用程序中,所有的字符串常量都集中、不重复的保存在一个表中,这个表叫做字符串“拘留表”。
一个String类型内部包含如下两个成员变量:
char[],字符数组,本质是个引用,指向字符串拘留表中的字符串,
int,字符串的哈希值
同样在64位环境下,关闭压缩,以如下代码为例
Java Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | | package com.shadowfaxghh.test; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; public class Main { public static void main(String[] args) { System.out.println(VM.current().details()); String str1=new String("abc"); String str2=new String("abcde"); System.out.println(ClassLayout.parseInstance(str1).toPrintable()); System.out.println(ClassLayout.parseInstance(str2).toPrintable()); } } |
输出如下
可以看到两个对象占用的内存空间大小是一致的
对象头占16个字节,char[]类型(引用类型)成员变量占8个字节,int类型成员变量占4个字节,填充4个字节,共32字节
不同的创建String对象的方式
我们经常会使用如下两种方式来创建String对象,但是这两种方式下的内存模型其实是不同的
Java Code
1 2 3 4 5 6 7 8 9 10 | | package com.shadowfaxghh.test; public class Main { public static void main(String[] args) { String str1=new String("abc"); String str2="abc"; System.out.println(str1==str2);//false } } |
通过观察其生成的.class文件中的字节码指令
可知他们的内存模型大体如下
对于String str1=new String("abc"); 是在堆上创建了一个String对象,设置这个对象,然后将这个String引用保存到局部变量中
对于String str2="abc"; 是直接将对运行时数据结构中的String引用保存到局部变量中
故两者是不相同的
String类还提供了intern()方法用于获取一个字符串在拘留表中的引用。
因为该表中没有重复项,所以任何字面相同的字符串的intern()方法返回总是相等的。以如下代码为例,其输出结果如注释所示
Java Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | | package com.shadowfaxghh.test; public class Main { public static void main(String[] args) { String a=new String("123"); String b=new String("123"); // String a=Integer.toString(1)+Integer.toString(2)+ Integer.toString(3);//效果跟上面一样 String c="123"; String d="123"; System.out.println(a==b);//false System.out.println(a.equals(b));//true System.out.println(a==c);//false System.out.println(a.equals(c));//true System.out.println(a.intern()==c);//true System.out.println(c==d);//true } } |
关于字符串拘留表的位置
在JDK1.6之前,字符串拘留表属于方法区的一部分,但是在JDK1.7以后,它被移动到了堆中进行管理。可以通过如下代码来验证
Java Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | | package com.shadowfaxghh.test; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { List<String> list=new ArrayList<String>(); int i=0; while(true) list.add(String.valueOf(i++).intern()); } } |
在运行时会发生溢出OutOfMemoryError,通过在不同的JDK版本下运行,根据错误中描述的位置,可以得知差异。
此外,字符串拘留表中的字符串也可能会被回收的
所以,虽然intern()返回的永远等于字符串字面量,但是并不代表每时每刻相同字符串的intern()返回的都是一样的
因为存在这样一种可能,在一次intern()调用以后,该字符串被后续被回收了,之后再进行一次intern()调用,那么相同的字符串字面量又重新加入拘留表,但是位置已经不同。
不可变性
不变性是指String对象一旦生成,则不能再对他进行改变
他的主要作用在于,当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而提升系统性能。
由于不变性,一些看起来像是修改的操作,其实都是通过产生新的字符串来完成的,比如说String.substring()、String.concat()方法
Java Code
1 2 3 4 5 6 7 8 9 10 11 | | package com.shadowfaxghh.test; public class Main { public static void main(String[] args) { String str1=new String("abcde"); String str2=str1.substring(1, 2); System.out.println(str1==str2);//false } } |
此外,我们也经常会使用“+”这样的写法,既然具有不变形,所以这些操作其实也是通过创建StringBuilder对象来完成的
Java Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | | package com.shadowfaxghh.test; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { String str1=new String("abcde"); str1=str1+"f"; // //相当于是 // StringBuilder sb1=new StringBuilder(str1); // sb1.append("f"); // str1=sb1.toString(); str1=str1+"g"+"h"; // //相当于是 // StringBuilder sb2=new StringBuilder(str1); // StringBuilder temp = sb2.append("g"); // System.out.println(temp==sb2);//StringBuilder是同一个对象//true // // sb2.append("h"); // str1=sb2.toString(); } } |
通过其字节码指令可知
是创建了StringBuilder对象,然后持续调用其append()方法,最后调用toString()生成一个新的String对象返回。