字节码层面解析String到底创建了几个对象以及String扩展之intern()方法

String到底创建了几个对象一直是面试所关注的重点,String对象的intern方法在许多文章中都是含糊不清,希望这篇文章能够让大家真正的理解这两个问题。看这篇文章需要一点jvm运行时数据区的理解才能够更好的理解。

创建了几个对象

问题主要就是一下两种情况

   String s1=new String("ab");
   String s2=new String("a")+new String("b");

new String(“ab”)

也不空讲直接上字节码(不要害怕看字节码,其实很简单的,面对字节码可以更清楚的知道jvm底层到底进行了什么操作),这里我使用的是idea插件jclasslic来查看,也可以通过命令行javap -v ClassName 查看。
在这里插入图片描述
第一行:new了一个String对象,这时候要注意的是属性值并没有初始化,也就是完成了加载和链接的过程。

第二行:dup就是duplication的缩写,表示复制一份的意思。因为接下来对这个String对象的引用还有两个操作,一是调用他的init方法(第四行,也就是调用构造器),其二是将该引用保存到局部变量表中(第五行),而操作数栈只具备出栈和入栈的功能,所以要保存两份。执行后操作数栈中就有两份该对象的引用。

第三行:ldc就是将常量池中的"ab"压入栈。

第四行:invokexxxxxx就是调用方法的意思,其中又分为调用虚方法和非虚方法以及动态方法,具体就不展开了。这里调用的是String的构造器方法完成初始化的过程,而上一步压入操作数栈的"ab"的引用作为String构造器的参数传入。

第五行:将指向堆中String对象的引用变量存储到局部变量表下标为1的位置。
分析之后我们就知道这里一共创建了两个对象
1.在堆中new String()(不在字符串常量池中)
2.在字符串常量池中创建一个‘ab’对象
在这里插入图片描述

new String(“a”)+new String(“b”);

在这里插入图片描述
这个字节码文件一下就长了很多啊。
第六行:new 一个StringBuilder,同样未完成初始化。(第一个对象)
第七行:复制一份StringBuilder的引用。
第八行:调用StringBuilder的构造器。
第九行:new 一个String。(第二个对象)
第十行:复制一份String的引用。
第十一行:从字符串常量池中加载"a"(第三个对象)
第十二行:调用String 的构造器。
第十三行:调用Stringbuilder的append方法,先将"a"拼接上去。
第十四行:再new一个String(第四个对象)
第十五行:复制一份。
第十六行:从字符串常量池中加载"b"(第五个对象)
第十七行:调用该String的构造器方法
第十八行:调用StringBuffer的append方法
第十九行:调用StringBuffer的toString方法(第六个对象,底层new String并返回,但是与new String(“ab”)不同的是,他的参数是数组,所以不会在常量池中加载"ab")代码如下

 public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

第二十行:将该String的引用存储在局部变量表的下标为2的位置
第二十一行:return
分析完毕之后我们发现这里一共创建了留个对象,其中
1个StringBuilder对象。
3个堆中常量池外的String对象"a",“b”,“ab”。
2个常量池中的对象"a",“b”,共六个对象。

并且我们发现在字符串常量池中并没有"ab"这个对象。这些结论是为接下来分析intern()方法的重要结论。

这时候内存中是这样的一个结构
在这里插入图片描述

intern()方法详解

我们先看api中对这个方法的介绍
在这里插入图片描述
大致意思就是调用时会返回一个String对象:当intern()方法被调用的时候,如果在字符串常量池中已经包含了一个使用equals()方法判断相等的String对象,那么就返回该String对象。否则这个String对象就会被添加到常量池中,然后返回该对象的一个引用。并且保证常量池中不会重复。(大家最好还是自己读一下英文版的啦)

这里就有一个需要关注的点了,不知道大家注意到了没有。就是上面加粗的部分,String对象添加到常量池中到底是什么方式,他并没有细说。我们能够想到的就是两种方式。(思考一下上面的案例二,这就是常量池外有这个String对象而常量池中没有的例子)

①在常量池中创建一个String对象并返回该对象的引用。

②在常量池中保存常量池外String对象的引用,并返回该引用。

jdk中也是用了这两种方法。在jdk6及以前使用的是方法①,jdk7及以后使用的是方法②。明显方法②是方法①的优化版。

这时候就需要案例来检验一下大家是否真正的理解了

案例一:

 String s1=new String("ab");
 String s2= s1.intern();
 System.out.println(s1==s2);

这题稍微简单,练手用的,由于在调用intern()方法时"ab"在字符串常量池中已经
存在,所以intern()方法直接返回该对象的引用。因此这时候数据是这样的
在这里插入图片描述
结果是false就很明确啦。

案例二:

 String s=new String("a")+new String("b");
 s.intern();
 String s1="ab";
 System.out.println(s==s1);

看到这里有点懵逼的可以回头看看前面的知识点哦!

根据前面的知识我们可以知道对象s调用intern()方法时,字符串常量池中并没有对象"ab",所以我们就需要执行将"ab"添加到字符串常量池的的操作。而这时在不同的jdk版本可能就有不同的操作。

jdk6及之前在常量池中创建一个String对象并返回该对象的引用。

所以很明显这时候数据是这样存储的(需要注意的是这时候字符串常量池是在方法区中,jdk1.7才将字符串常量池移到堆中,一开始我也没有想到这一点,我是在这篇博客快完成的时候才发现这个问题的 ̄□ ̄||),所以结果就是false。
在这里插入图片描述
jdk7及之后在常量池中保存常量池外String对象的引用,并返回该引用。
这时候数据是这样存储的
在这里插入图片描述
所以这时候就是true。

案例二结论
jdk6及以前false;
jdk7及以后true;

讨论区

最后再来讨论讨论intern()除了面试之外对我们的代码有什么帮助呢?大家可以自己思考一下,有自己更多理解的欢迎评论区指出哦,我也是自己瞎想的(没有工作经验的应届生小白恳请大佬指点)。
1. 有需要通过集合,数组等容器中数据创建字符串对象时,可以调用intern方法在字符串常量池中保存该对象的引用,下一次创建相同的对象时,可以直接返回该引用。
我们使用案例来模拟一下动态使用容器生成大量字符串的过程,再使用jprfiler来查看一下到底有多少个String对象。

//不加intern()方法的案例
       int []data=new int[]{3,5,6,7,4,1,9,2};
       //每创建一个String就添加到list中,防止被垃圾回收
       ArrayList<String> list=new ArrayList<>();
       for (int i = 0; i < 1000_0000; i++) {
       //为什么这里没有new String(),一开始我的案例也是想new String的但是发现没有传int值的构造器 ̄□ ̄||,然后就只能用String.valueOf()方法,然后发现他底层返回new String所以我就不用再new了,底层代码如下
           list.add(String.valueOf(data[i%data.length]));
       }
//String的其中一个valueOf()方法
 public static String valueOf(int i) {
        return Integer.toString(i);
    }
    //Integer 类的toString方法
  public static String toString(int i) {
        if (i == Integer.MIN_VALUE)
            return "-2147483648";
        int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
        char[] buf = new char[size];
        getChars(i, size, buf);
      //在这里new了一个String,如果在外面再new一个的话就显得不是那么专业,哈哈
        return new String(buf, true);
    }

结果我们看到有10005372个String对象(为什么char[]类型的也那么多就不用我说了吧)
在这里插入图片描述

//加intern()方法的案例
       int []data=new int[]{3,5,6,7,4,1,9,2};
       ArrayList<String> list=new ArrayList<>();
       for (int i = 0; i < 1000_0000; i++) {
           list.add(String.valueOf(data[i%data.length]));
       }

加了intern方法后我们发现同样的结果,但是只创建了775277个String对象,直接少了十倍还多。
在这里插入图片描述
而在大小上18606kb≈18mb,而没用intern()却使用了240mb,省下了好多的空间。

最后的最后

再来几个小问题练练手

 String s=new String("a")+new String("b");
 String s1="ab";
 s.intern();
 System.out.println(s==s1);//jdk6:false    jdk7:false
 String s=new String("a")+new String("b");
 s.intern();
 String s1="ab";
 System.out.println(s==s1);//jdk6 :false    jdk7:true

能够完全理解这最后这两个案例的话,那么恭喜啦!说明你面试再也不怕问创建几个对象啦!!!intern方法也不用再怕啦!!
more about String:关于String底层使用的是char数组还是byte数组以及一点String面试问题

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值