前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JDK字符串存储机制及String#intern方法深入研究

JDK字符串存储机制及String#intern方法深入研究

作者头像
saintyyu
发布2021-11-22 09:53:35
2940
发布2021-11-22 09:53:35
举报
文章被收录于专栏:IT专栏

在jdk7或jdk8中执行如下代码(执行结果见对应的注释行):

代码语言:javascript
复制
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);//jdk6、jdk7、jdk8都为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);//true
        System.out.println(s11 == s12);//true
        System.out.println("======");
}

上面的测试代码进行了两组对比,如果你完全能理解执行的结果,那么恭喜你,这篇博客你没必要看了;反之,这篇博客接下来的内容就是你的菜。不过,即使你能答对执行结果,也建议你阅读下本文对常量池及字符串创建的分析,因为关于这块解读的博客确实非常多,但可惜的是,大多说法都是错误的!!!

常量池

在分析这两组对比结果之前,我们需要先了解常量池。常量池大体可以分为:静态常量池,运行时常量池。

  • 静态常量池 存在于class文件中,比如执行“javap -verbose java类名”命令后,看到的最前面的部分内容就是静态常量池。
  • 运行时常量池 class文件被加载进了内存之后,静态常量池中的数据会保存在运行时常量池中,而运行时常量池保存在方法区中。通常说的常量池指的是运行时常量池。

运行时常量池

用于存放编译期生成的各种字面量和符号引用,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

  • JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。
  • JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  • JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)。

字符串常量池

了解了字符串常量池,我们再来回答一个问题:字符串常量池里存的到底是对象还是引用?在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区(也就是方法区)中,此时字符串常量池中存储的是对象在 JDK7.0版本,字符串常量池被移到了堆中了,因而字符串常量池中存储的就只是引用了;在 JDK8.0 版本,永久代(方法区)被元空间取代了,字符串常量池仍然在堆中,因而字符串常量池中存储的也是引用。正是以上的差异,导致上述代码在jdk6中和jdk7和jdk8中的执行结果是不同的,下面具体分析。

字符串的三种生成方式

我们接着来介绍字符串的生成方式。很多博客说Java字符串生成有两种方式,但本博客总结为三种:

以双引号的方式:

以new String()的方式:

以运算符(+)创建:

String#intern()方法

由前面的介绍可知,在有些场景下,字符串在编译结束后其值是无法确定的, 因而无法在类加载阶段将这样的字符串添加到字符串常量池中。intern方法就是为了解决这样的问题而诞生的,即该方法可以在运行过程中手动将字符串添加进字符串常量池。

在该方法的源码中有这样一段注释:

代码语言:javascript
复制
When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.

实际上,这段话对jdk6来说是合适的,但到了jdk7和jdk8就容易引起误会了。具体来说就是,在jdk6中,如果常量池中不包含值相等的字符串常量,则在常量池中重新创建一个该值的对象,该对象和堆中的对象没有任何关联;对于jdk7和jdk8时,如果常量池中不包含值相等的字符串常量时,是直接将堆中该对象的引用直接添加到常量池中,因而此时从常量池中获取到的对象就是堆中的对象。

测试代码分析   分别分析jdk6和jdk7,8

代码语言:javascript
复制
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

由前面的介绍可知,s1在代码执行阶段指向的是堆中值为“12”的对象,而此时字符串常量池中是没有该对象引用的;第二行代码在类加载阶段会在堆中创建一个值为“12”的对象,并将其引用添加到字符串常量池中,而s2指向的就是类加载阶段创建的对象;第三行代码执行intern()方法时,发现常量池中已经有值为“12”的对象引用了,于是返回的是字符串常量池中对象的引用,即s2和s3指向的是同一个对象,因而有s2==s3为true,而s1指向的是堆中另一个对象,因而s1==s3为false。

代码语言:javascript
复制
String s4 = "4" + "5";
String s5 = "45";
String s6 = s4.intern();
System.out.println(s4 == s6);//true
System.out.println(s5 == s6);//true

类似地,s4指向的对象是在类加载阶段创建的值为“45”的对象,且此时字符串常量池中也引用了该对象;第二句,s5获取到的是字符串常量池中引用的对象,即s4和s5指向了同一个对象。第三句执行intern方法时发现常量池已经有该对象了,返回的还是常量池的对象,因而才会有s4==s5==s6都为true。

代码语言:javascript
复制
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

类似地,s7指向的是位于堆中值为“78”的对象,此时常量池中还没有值为“78”的对象;执行第二句时,s8获取到的是字符串常量池中引用的对象;执行intern方法时,返回了常量池中的对象引用,因而s7==s9为false,而s8==s9为true。

代码语言:javascript
复制
String s10 = new String("9") + new String("0");
String s11 = s10.intern();
String s12 = "90";
System.out.println(s10 == s12);//true
System.out.println(s11 == s12);//true

类似地,s10指向的是位于堆中值为“90”的对象,此时常量池中还没有值为“90”的对象;此时执行intern方法,发现常量池中还没有值为“90”的对象,于是将对中值为“90”的对象引用添加(add)到字符串常量池中,并返回该对象的引用(即s11和s10指向的都是堆中的对象);执行第三句时,发现常量池中已经有该对象的引用了,则直接返回该引用,因而才会有s10==s11==s12都为true。

String#intern()方法使用分析

美团技术团队给了一个正确使用intern方法的示例:

代码语言:javascript
复制
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
	long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
        //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));//不使用intern方法
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();//使用intern方法
    }

	System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

在该示例中,实际上只使用了10个值不同的字符串,但通过分别注释掉不使用intern方法和使用intern方法这两句代码发现,不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间,而使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。

看到这里不知道你会不会和博主有同样的困惑:怎么可能呢?虽然使用了intern方法,可是不还是通过new String()的方式创建的对象么?new String().intern()方法肯定是先在堆中创建个对象,然后再尝试将该字符串引用添加到常量池中呀,那为什么会和不使用intern方法有这么大差异呢?难道美团技术团队写错了?

对于这个困惑,博主也是百思不得其解(甚至开始怀疑人生)了好几天,后来突然灵光乍现:是GC,肯定是GC!!什么意思呢?没错,上面的示例中,不论是否使用intern方法,两者都是先使用new String()的方式来创建的字符串对象,因而如果没有GC的化,两种方式创建字符串对象肯定是一样多的。而有了GC之后,两者的情形就大不一样了。对于不使用intern方法时,new String()返回的对象是被arr[i]所引用的,因而在GC时是不会被回收的;相反,使用intern方法后,由于上述代码实际上只使用了10个值不同的字符串,因而在绝大多数情况下,attr[i]中保存的都是同一个对象的引用,而此时new String().intern()语句产生的对象本身成了匿名对象,在GC过程中就被当成垃圾给回收了。因而才会出现这么大的差距。

其实由这个示例可知,intern方法适合在字符串变化不频繁的场景下使用;对于大量值不同的字符串,如果使用intern方法则会导致常量池hashTable中存储过多字符串引用,从而导致YGC变长等问题,具体参见参考博客

总结

本文深入分析了jdk8字符串的存储机制及String#intern方法的功能,其目的是为了梳理虚拟机对字符串的处理机制,为我们在使用字符串时具体选择哪种方式来产生字符串提供依据。此外,对于intern方法的适用场景可以总结为:适合于变化不频繁的字符串。

参考博客:

1、https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html  深入解析String#intern

2、https://blog.csdn.net/goldenfish1919/article/details/81216560 JVM的方法区和永久带是什么关系

3、https://blog.csdn.net/liupeifeng3514/article/details/81067150  源码学习 | String 之 new String()和 intern()方法深入分析

4、https://blog.csdn.net/Herishwater/article/details/100924191  Java 中方法区与常量池

5、https://zhuanlan.zhihu.com/p/107781993  Java 基础:String——常量池与 intern

6、https://cloud.tencent.com/developer/article/1450501 彻底弄懂java中的常量池

7、http://lovestblog.cn/blog/2016/11/06/string-intern/ JVM源码分析之String.intern()导致的YGC不断变长

8、https://blog.csdn.net/zm13007310400/article/details/77534349 Java中的常量池(字符串常量池、class常量池和运行时常量池)

9、https://www.jianshu.com/p/e74fe532e35e  JVM知识整理

10、https://blog.csdn.net/xiaojin21cen/article/details/105300521?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~first_rank_v2~rank_v25-3-105300521.nonecase  class常量池、字符串常量池和运行时常量池的区别

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/02/24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 常量池
  • 运行时常量池
  • 字符串常量池
  • 字符串的三种生成方式
  • String#intern()方法
  • 测试代码分析   分别分析jdk6和jdk7,8
  • String#intern()方法使用分析
  • 总结
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档