注册

Java字符串常量池和intern方法解析

Java字符串常量池和intern方法解析


这篇文章,来讨论一下Java中的字符串常量池以及Intern方法.这里我们主要讨论的是jdk1.7,jdk1.8版本的实现.


字符串常量池


在日常开发中,我们使用字符串非常的频繁,我们经常会写下类似如下的代码:



  • String s = "abc";
  • String str = s + "def";

通常,我们一般不会这么写:String s = new String("jkl"),但其实这么写和上面的写法还是有很多区别的.


先思考一个问题,为什么要有字符串常量池这种概念?原因是字符串常量既然是不变的,那么完全就可以复用它,而不用再重新去浪费空间存储一个完全相同的字符串.字符串常量池是用于存放字符串常量的地方,在Java7和Java8中字符串常量池是堆的一部分.


假如我们有如下代码:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");

那么从内存分配的角度上看,最终会有哪些字符串生成呢,首先我先给出一张图来代表最终的结论,然后再分析一下具体的原因:


image.png


现在来依次分析上面的代码的执行流程:




  1. 执行String s = "abc",此时遇到"abc"这个字符串常量,会在字符串常量池中完成分配,并且会将引用赋值给s,因此这条语句会在字符串常量池中分配一个"abc".(这里其实没有空格,是因为生成文章时出现了空格,下文中如果出现同样情况,请忽略空格)




  2. 执行String s1 = s + "def",其实这个语句看似简单,实则另有玄机,它其实最终编译而成的代码是这样的:String s1 = new StringBuilder("abc").append("def").toString().首先在这个语句中有两个字符串常量:"abc"和"def",所以在字符串常量池中应该放置"abc"和"def",但是上个步骤已经有"abc"了,所以只会放置"def".另外,new StringBuilder("abc")这个语句相当于在堆上分配了一个对象,如果是new出来的,是在堆上分配字符串,是无法共享字符串常量池里面的字符串的,也就是说分配到堆上的字符串都会有新的内存空间. 最后toString()也是在堆中分配对象(可以从源码中看到这个动作),最终相当于执行了new String("abcdef");所以总结起来,这条语句分析起来还是挺麻烦的,它分配了以下对象:



    • 在字符串常量池分配"abc",但本来就有一个"abc"了,所以不需要分配
    • 在字符串常量池中分配“def"
    • 在堆中分配了"abc"
    • 在堆中分配了"abcdef"



  3. 执行String s2 = new String("abc").首先有个字符串常量"abc",需要分配到字符串常量池,但是字符串常量池中已经有"abc"了,所以无需分配.因此new String("abc")最终在堆上分配了一个"abc".所以总结起来就是,在堆中分配了一个"abc"




  4. 执行String s3 = new String("def");.首先有个字符串常量"def",需要分配到字符串常量池,但是字符串常量池中已经有"def"了,所以无需分配.因此new String("def")最终在堆上分配了一个"def".所以总结起来就是,在堆中分配了一个"def"。




总结起来,全部语句执行后分配的对象如下:



  • 在堆中分配了两个"abc",一个"abcdef",一个"def"
  • 在字符串常量池中分配了一个"abc",一个"def"

也就是图中所表示的这些对象,如果明白了对象是如何分配的,我们就可以分析以下代码的结果:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");
String s4 = "abcdef";
String s5 = "abc";
System.out.println(s == s2); //false 前者引用的对象在字符串常量池 后者在堆上
System.out.println(s == s5);; //true 都引用了字符串常量池中的"abc"
System.out.println(s1 == s4); //false 前者引用的对象在字符串常量池,后者在堆上

intern方法


在字符串对象中,有一个intern方法.在jdk1.7,jdk1.8中,它的定义是如果调用这个方法时,在字符串常量池中有对应的字符串,那么返回字符串常量池中的引用,否则返回调用时相应对象的引用,也就是说intern方法在jdk1.7,jdk1.8中只会复用某个字符串的引用,这个引用可以是对堆内存中字符串中的引用,也可能是对字符串常量池中字符串的引用.这里通过一个例子来说明,假如我们有下面这段代码:


String str = new String("abc");
String str2 = str.intern();
String str3 = new StringBuilder("abc").append("def").toString();
String str4 = str3.intern();
System.out.println(str == str2);
System.out.println(str3 == str4);  

那么str2和str以及str3和str4是否相等呢?如果理解了上面对字符串常量池的分析,那么我们可以明白在这段代码中,字符串在内存中是这么分配的:



  • 在堆中分配两个"abc",一个“abcdef"
  • 在字符串常量池中分配一个"def",一个"abc"


  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;


  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此str4会指向堆中的"abcdef",因此str3等于str4,我们会发现一个有意思的地方:如果将第三句改成String str3 = new StringBuilder("abcdef").toString();,也就是把append后面的字符串和前面的字符串做一个拼接,那么结果就会变成str3不等于str4.所以这两种写法的区别还是挺大的.

要注意的是,在jdk1.6中intern的定义是如果字符串常量池中没有对应的字符串,那么就在字符串常量池中创建一个字符串,然后返回字符串常量池中的引用,也就是说在jdk1.6中,intern方法返回的对象始终都是指向字符串常量池的.如果上面的代码在jdk1.6中运行,那么就会得到两个false,原因如下:



  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;


  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此执行intern方法会在字符串常量池中分配"abcdef",然后str4最终等于这个字符串的引用,因此str3不等于str4,因为上面的str3指向堆,而str4指向字符串常量池,所以两者一定不会相等.

深入理解JVM虚拟机一书中,就有类似的代码:


String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1 == str1.intern());
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2 == str2.intern());

在jdk1.6中,两个判断都为false.因为str1和str2都指向堆,而intern方法得出来的引用都指向字符串常量池,所以不会相等,和上面叙述的结论是一样的.在jdk1.7中,第一个是true,第二个是false.道理其实也和上述所讲的是一样的,对于第一个语句,最终会在堆上创建一个"计算机软件"的字符串,执行str1.intern()方法时,先在字符串常量池中寻找字符串,但没有找到,所以会直接引用堆上的这个"计算机软件",因此第一个语句会返回true,因为最终都是指向堆.而对于第三个语句,因为和第一个语句差不多,按理说最终比较也应该返回true.但实际上,str2.intern方法执行的时候,在字符串常量池中是可以找到"java"这个字符串的,这是因为在Java初始化环境去加载类的时候(执行main方法之前),已经有一个叫做"java"的字符串进入了字符串常量池,因此str2.intern方法返回的引用是指向字符串常量池的,所以最终判断的结果是false,因为一个指向堆,一个指向字符串常量池.


总结


从上面的分析看来,字符串常量池并不像是那种很简单的概念,要深刻理解字符串常量池,至少需要理解以下几点:



  • 理解字符串会在哪个内存区域存放
  • 理解遇到字符串常量会发生什么
  • 理解new String或者是new StringBuilder产生的对象会在哪里存放
  • 理解字符串拼接操作+最终编译出来的语句是什么样子的
  • 理解toString方法会发生什么

这几点都在本文章中覆盖了,相信理解了这几点之后一定对字符串常量池有一个更深刻的理解.其实这篇文章的编写原因是因为阅读深入理解JVM虚拟机这本书的例子时突然发现作者所说的和我所想的是不一样的,但是书上可能对这方面没有展开叙述,所以我去查了点资料,然后写了一些代码来验证,最终决定写一篇文章来记录一下自己的理解,在编写代码过程中,还发现了一个分析对象内存地址的类库,我放在参考资料中了.


参考资料


http://www.baeldung.com/java-object… 查看java对象内存地址


作者:JL8
来源:juejin.cn/post/7237827233351073849

0 个评论

要回复文章请先登录注册