java中String的创建与存储机制
介绍
本文开头给了一段测试代码,基于此介绍了intern方法,由intern方法引出了字符串常量池和StringTable,接着用表格的方式探讨了HotSpot实现的jdk6和jdk7中字符串常量池的存储差异,介绍完字符串常量池后,也用表格介绍了三种字符串创建的方式,最后再对测试代码的结果逐一分析,加深对原理的理解。
测试代码,全文基于该代码展开
public class StringTest {
public static void main(String[] args) {
System.out.println("第一组对比:");
System.out.println("======");
String s1 = "1" + new String("2");
String s2 = "12";
String s3 = s1.intern();
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//true
System.out.println("======");
String s4 = "4" + "5";
String s5 = "45";
String s6 = s4.intern();
System.out.println(s4 == s6);//true
System.out.println(s5 == s6);//true
System.out.println("======");
System.out.println();
System.out.println("第二组对比:");
System.out.println("======");
String s7 = new String("7") + new String("8");
String s8 = "78";
String s9 = s7.intern();
System.out.println(s7 == s9);//false
System.out.println(s8 == s9);//true
System.out.println("======");
String s10 = new String("9") + new String("0");
String s11 = s10.intern();
String s12 = "90";
System.out.println(s10 == s12);// jdk6 false jdk7 jdk8 true
System.out.println(s11 == s12);//true
System.out.println("======");
}
}
代码运行环境
我在Intellij安装了jdk6、jdk7、jdk8,代码在三种条件下都运行了一遍查看结果
结果证明,只有 s10==s12一处有不同的结果,jdk6下为false,jdk7和jdk8都为true,这个差异我会在下文仔细探讨。
String#intern方法介绍
可以看到代码中了了String#intern方法,我们去查看String类中intern的源码,可以得到以下注释
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class <code>String</code>.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this <code>String</code> object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this <code>String</code> object is added to the
* pool and a reference to this <code>String</code> object is returned.
* <p>
* It follows that for any two strings <code>s</code> and <code>t</code>,
* <code>s.intern() == t.intern()</code> is <code>true</code>
* if and only if <code>s.equals(t)</code> is <code>true</code>.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
翻译过来意思就是,提供一个由String类维护的开始为空的字符串常量池(String Constant Pool),调用intern方法时,如果字符串常量池中已经有一个相同字符串时,返回已有字符串的引用,如果池中不存在相同字符串,则把新字符串加入池中并返回引用。所有Literal strings(双引号形式的字符串)和字符串常量表达式都存在字符串常量池中。
Intern方法的作用在于在程序运行期间动态维护一个字符串常量池,池中保证值相同的字符串只有一个,即池中不可能有两个值相同的字符串。
在程序中需要频繁使用相同值的字符串时,使用intern方法相较于new可以减少内存使用(new方法可以生成多个同值字符串),详细可见参考1中美团技术团队给的例子
注意:intern方法是native方法,即intern不是用java实现的,而是jvm通过JNI调用的平台方法,所以intern的功能可能会由于所在的平台不同而有些许不同。
关于String Constant Pool(字符串常量池)与StringTable(字符串表)
intern方法中提到了字符串常量池,英文为String Constant Pool,java的常量池有好几种,我看到有的博文直接把字符串常量池简化为常量池,容易引起误解,特此给出全称。
字符串常量池是一块存储字符串对象的特定内存区域,但字符串除了存在字符串常量池,还可以用new的方法存在于java堆中。
intern方法在原理上是通过维护一个hashtable来保证字符串的唯一性,这个table就是Stringtable,
StringTable和字符串常量池的关系如下:
StringTable存储的元素类似于<key,一个字符串常量池中字符串的引用> 。intern通过查询key值来查看是否已经有同值的字符串,字符串常量池中所有的字符串都在StringTable中有引用。
字符串常量池是一个概念上的内存区域,存储字符串实际对象(这片区域所在位置不同jdk有所不同,在下一节会讨论)StringTable则是使用字符串常量池的技术细节。
我们说一个字符串在字符串常量池中,其实是指StringTable中有对字符串的引用
由于String#intern是native方法,所以它需要用到的StringTable也不会是存在java堆里的,实际上StringTable存在与java的Native Memory中,不受java的GC策略影响,[参考2和参考3]
jdk6 jdk7中字符串常量池的区别
jdk版本 | String Constant Pool所在位置 | String Constant Pool大小限制 | StringTable所在位置 |
---|---|---|---|
jdk6及之前 | 方法区,由永久代(PermGen)实现 | 默认大小为1009,早期版本无法修改,在Java6u30到Java6u41版本中才可以配置,所以不建议在jdk6中使用string#intern方法 【参考4】 | Native Memory |
jdk7及之后 | java堆 | 运行时前可通过-XX:StringTableSize=N 来设置StringTable大小,N为了性能考虑可设置为质数,N的理论上限是java堆的大小 | Native Memory |
String的两处存储位置与三种创建方式
String对象的存储位置只有两个,一个是java堆,一个是字符串常量池(jdk7后字符串常量池实际上也在java堆中,但StringTable只引用了java堆中一部分的字符串,保证引用的字符串中无同值的字符串,也就造成了理论上的字符串常量池与java堆中其他区域的划分)
String的创建可分为三种大类,四种小类,见下表。
创建方式 | 例子 | String存储位置 | 备注 |
---|---|---|---|
双引号创建 | “ab” | 字符串常量池 | 常量池中只会有一个"ab",String str1=“ab”; String str2=“ab”; str1 str2指向同一个字符串 |
new创建 | new String(“ab”) | java堆 | new String(“ab”)中因为同时存在双引号的字符串,所以上述语句也会在字符串常量池中留下一个"ab"字符串。new可以创建多个值相同地址不同的字符串,String str1=new String(“ab”);String str2=new String(“ab”);str1和str2的地址是不同的,但二者都不存在于StringTable中 |
字符串拼接创建1 | “a”+“b” | 字符串常量池 | 在类加载阶段会生成一个"ab"对象随后放入字符串常量池,此时字符串常量池中不会有"a"和"b"的对象 |
字符串拼接创建2 | String str1 = new String(“a”) + new String(“b”);String str2 = “c” + new String(“d”);String str3 = “e”;String str4 = “f”;String str5 = str3 + str4; | java堆 | 上述拼接方式会被优化为新建一个StringBuilder并调用append方法返回拼接后的字符串,StringBuilder生成的字符串存储于java堆中,另外,例子中StringTable只有单个字母的引用,不会有两个字母的引用 |
测试代码分析
介绍完需要的背景知识,我们可以开始对开头的代码进行分析了,这里再贴一次代码
public class StringTest {
public static void main(String[] args) {
System.out.println("第一组对比:");
System.out.println("======");
String s1 = "1" + new String("2");
String s2 = "12";
String s3 = s1.intern();
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//true
System.out.println("======");
String s4 = "4" + "5";
String s5 = "45";
String s6 = s4.intern();
System.out.println(s4 == s6);//true
System.out.println(s5 == s6);//true
System.out.println("======");
System.out.println();
System.out.println("第二组对比:");
System.out.println("======");
String s7 = new String("7") + new String("8");
String s8 = "78";
String s9 = s7.intern();
System.out.println(s7 == s9);//false
System.out.println(s8 == s9);//true
System.out.println("======");
String s10 = new String("9") + new String("0");
String s11 = s10.intern();
String s12 = "90";
System.out.println(s10 == s12);// jdk6 false jdk7 jdk8 true
System.out.println(s11 == s12);//true
System.out.println("======");
}
}
s1是拼接的方式,通过StringBuilder存储在java堆中,s2通过双引号的方式存储在字符串常量池中,因为池中(以下会用池表示字符串常量池)已经有"12",所以s3通过intern从池中拿到对"12"的引用,这个引用跟s2为同一地址,所以s1==s3 false s2==s3 true
s4在类加载阶段成为"45",并存储池中,s5通过双引号的方式创建,会先判断池中是否已有"45",判断有,s5为池中"45"的引用,s6 =s4.intern()也是池中"45"的引用,所以s4==s6 true s5==s6 true,可推出s4==s5 true
s7 s8 s9和s1 s2 s3的分析一致
最后一片代码为jdk6 jdk7 jdk8上的结果有所不同,jdk6中s10==s12为false,s10是在java堆中的"90",s11=s10.intern()使得方法区中的字符串常量池新添了一个"90"对象,并把引用存在了StringTable。s12=“90"会去查StringTable,发现有,于是s12是指向字符串常量池中"90"的引用。 而在jdk7 jdk8实测s10==s12都为true,s10是在java堆中的"90”,s11=s10.intern()时因为知道java堆中已经有了"90",所以不必再在java堆上new一个"90",intern方法直接把一个跟s10引用内容一样的引用更新到StringTable中,所以s11==s10会是true,s12="90"则直接从StringTable中拿到"90"的引用,所以s10==s12为true. jdk6和jdk7 jdk8因为字符串常量池所在的位置不同,最终导致了s11==s10的结果的不同
总结
本文通过探讨String#intern方法以及字符串常量池和StringTable,深入介绍了jdk6和jdk7中字符串的存储原理与差异,为我们在业务场景中选择哪种方式产生字符串提供了理论参考,比如intern方法就适合用于少量字符串多次重复利用的场景。
参考
-
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html 美团技术团队
-
https://www.zhihu.com/question/49044988 第一个回答的评论区
-
https://www.baeldung.com/native-memory-tracking-in-jvm 2.5节加粗字体
-
http://java-performance.info/string-intern-in-java-6-7-8/
-
https://stackoverflow.com/questions/10578984/what-is-java-string-interning/10579062#10579062
6.http://xmlandmore.blogspot.com/2013/05/understanding-string-table-size-in.html