探讨java中String的创建与存储机制

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()&nbsp;==&nbsp;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&trade; 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"的对象
字符串拼接创建2String 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方法就适合用于少量字符串多次重复利用的场景。

参考

  1. https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html 美团技术团队

  2. https://www.zhihu.com/question/49044988 第一个回答的评论区

  3. https://www.baeldung.com/native-memory-tracking-in-jvm 2.5节加粗字体

  4. http://java-performance.info/string-intern-in-java-6-7-8/

  5. 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

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值