String s = new String("xyz")建立了几个实例你真的能答对吗?

从面试题提及

String s = new String("xyz"); 建立了几个实例?

这是一道很经典的面试题,在一本所谓的Java宝典上,我看到的“标准答案”是这样的:java

两个,一个堆区的“xyz”,一个栈区指向“xyz”的s。

这个所谓的“标准答案”槽点太多,后面咱们慢慢分析。git

虽然答案很离谱,可是我以为这个问题自己也不具备什么意义,由于问题没有既定义“建立”的具体含义,又没有指定“建立”的时间,是运行时吗?包不包括类加载的时候?有没有上下文代码语境?也没有定义实例是指什么实例,是指Java实例吗?仍是单指String实例?包不包括JVM中的C++实例?github

显然,这个问题是一个“有问题的问题”。这个答案也是一个“有问题的答案”。面试

String结构

在分析以前,为了方便后面画内存图,咱们须要对Java中的String结构有一个大体了解:数组

从上图能够看出,String类有三个属性:缓存

  • value:char数组,用于用于存储字符。微信

  • hash:缓存字符串的哈希码,默认为0(String的hash值在真正调用hashCode方法的时候才会去计算)。oracle

  • serialVersionUID:序列化用的。jvm

正常的问题与合理的解释

在上面的题干上加上一些限定词,能够获得一个新的问题:oop

String s = new String("xyz");建立几个String实例?

对于这个问题,在网上能找到一些比较高赞的答案:

两个。
一个是字符串字面量"xyz"所对应的、存在于全局共享的常量池中的实例,
另外一个是经过new String(String)建立并初始化的、内容(字符)与"xyz"相同的实例。
考虑到若是常量池中若是有这个字符串,就只会建立一个。
同时在栈区还会有一个对new出来的String实例的s。

考虑到了栈与堆,提到了常量池,我认为这已经达到大部分面试官对这个题目答案的期许了,或许这也是面试官想要考察的点。

但这个答案也仅是比较合理,并不彻底正确。

首先,我不理解的是为何不少答主老是用“常量池”来代替“字符串常量池”,在Java体系中,实际上是有三个常量池的,三个常量池的概念和用处都不相同,混淆在一块儿容易给别人形成误解。

其次,就算答主说的“常量池”就是“字符串常量池”,可“字符串常量池”中存的是String实例的引用,而不是字符串,这是有很大区别的。并且这个答案是没有考虑代码执行的环境。

这些问题,下面都会一一分析。

分清变量和实例

咱们先回到开头的问题与“标准答案” :

问题:String s = new String("xyz"); 建立了几个实例?
答案:两个,一个堆区的“xyz”,一个栈区指向“xyz”的s

很明显写答案的人没有把变量和实例分清楚。在Java里,变量就是变量,类型的变量只是对某个对象实例或者null的,不是实例自己。声明变量的个数跟建立实例的个数没有必然关系。

举个例子:

String s1 = "xyz";  
String s2 = s1.concat("");  
String s3 = null;  
new String(s1);

这段代码会涉及3个String类型的变量:

  • s1,指向下面String实例的1
  • s2,指向与s1相同
  • s3,值为null,不指向任何实例

以及3个String实例:

  • "xyz"字面量对应的驻留的字符串常量的String实例
  • ""字面量对应的驻留的字符串常量的String实例
  • 经过new String(String)建立的新String实例,没有任何变量指向它

类加载

对于String s = new String("xyz");建立几个String实例?这个问题。

彷佛网上的全部答案都把类加载过程和实际执行过程合在一块儿分析的。

看起来好像是没有什么问题的,由于想要执行某个代码片断,其所在的类必然要被加载,并且对于同一个类加载器,最多加载一次。

可是咱们看一下这段代码的字节码:

彷佛只出现了一次new java/lang/String,也就是只建立了一个String实例。也就是说原问题中的代码在每执行一次只会新建立一个String实例。 这里的ldc指令只是把先前在类加载过程当中已经建立好的一个String实例("xyz")的一个引用压到操做数栈顶而已,并无建立新的String实例。

不是应该有两个实例吗?还有一个String实例是在何时建立的呢?

咱们都知道类加载的解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,根据JVM规范,符合规范的JVM实现应该在类加载的过程当中建立并驻留一个String实例做为常量来对应"xyz"字面量,具体是在类加载的解析阶段进行的。这个常量是全局共享的,只在先前还没有有内容相同的字符串驻留过的前提下才须要建立新的String实例。

因此你能够理解成,在类加载的解析阶段,其实已经建立了一个String实例,执行代码的时候,又new了一个String实例。固然,你把二者放在一块儿讨论并不会有什么问题。

JVM优化

以上讨论都只是针对规范所定义的Java语言与Java虚拟机而言。概念上是如此,但实际的JVM实现能够作得更优化,原问题中的代码片断有可能在实际执行的时候一个String实例也不会完整建立(没有分配空间)。

不结合上下文代码来看就直接说是“标准答案”就是耍流氓。

咱们看下这段代码:

运行这段代码,会不断的建立String对象吃内存,而后频繁的形成GC。

对于这个结论相信你们都没有意见,咱们加上-XX:+PrintGC -XX:-DoEscapeAnalysis打印日志,关闭逃逸分析(JDK8默认开启此优化,咱们先关闭)

运行一下看看:

结果确实如咱们所料,不断的建立String对象吃内存致使频繁GC。

咱们如今将-XX:-DoEscapeAnalysis改为-XX:+DoEscapeAnalysis,从新跑一下这段代码:

神奇的事情发生了,继续跑下去也没有再打出GC日志了。难道新建立String对象都不吃内存了么?

实际状况是:通过HotSpot VM的的优化后,newString()方法不会新建立String实例了。这样天然不吃内存,也就再也不触发GC了。

如今再来看开篇的那个问题,不结合具体状况,还能简单的说String s = new String("xyz");会建立两个String实例吗?

我只是举了一个逃逸分析的例子,HotSpot VM还有不少像这样的优化,好比方法内联、标量替换和无用代码削除。

klass-oop

若是题干上没有加上“Java”实例的定语,那JVM中的oop实例咱们也不该该忽略。

为了后面能更好的说清楚这一点,须要补充一下klass-opp模型的知识。先作一个约定,全文只要涉及JVM具体实现的内容都是基于Jdk8中HotSpot VM展开的。

HotSpot VM是基于C++实现,而C++是一门面向对象的语言,自己是具有面向对象基本特征的,因此Java中的对象表示,最简单的作法是为每一个Java类生成一个C++类与之对应。但HotSpot VM并无这么作,而是设计了一套klass-oop模型。

klass,它是Java类的元信息在JVM中的存在形式。一个Java类被JVM类加载器加载以后,就是以klass的形式存在于JVM之中。

oop,它是Java对象在JVM中的存在形式。每建立一个新的对象,在JVM内部就会相应地建立一个对应类型的OOP对象。

其中instanceOopDesc表示非数组对象,arrayOopDesc表示数组对象;

而objArrayOopDesc表示引用类型数组对象,typeArrayOopDesc表示基本类型数组对象。

举个例子:Java中String类的一个实例,在JVM中会有一个对应的instanceOopDesc实例。

字符串常量池

在Java体系中,有三种常量池:

  • class字节码中的常量池:存在于硬盘上。主要存放两大类常量:字面量、符号引用。

  • 运行时常量池:方法区的一部分。咱们常说的常量池,就是指这一块区域:方法区中的运行时常量池。

  • 字符串常量池:存在于堆区。这个常量池在JVM层面就是一个StringTable,只存储对java.lang.String实例的引用,而不存储String对象的内容。通常咱们说一个字符串进入了字符串常量池实际上是说在这个StringTable中保存了对它的引用,反之,若是说没有在其中就是说StringTable中没有对它的引用。

今天,咱们要了解的是字符串常量池。

字符串常量池,即String Pool。在JVM中对应的类是StringTable,底层实现是一个Hashtable。利用的是哈希思想。

下面这段代码,是往字符串常量池添加字符串方法。虽然是C++代码,但我相信学过Java的人都能看懂,至少也能明白这段代码干了什么事情。会经过String的内容+长度生成的hash值定位下标index,而后将Java的String类的实例对应的instanceOopDesc封装成HashtableEntry做为存储结构存储到常量池。

补充完字符串常量池的知识以后,咱们再回到文章开头的那一题:

String s = new String("xyz");建立了几个实例?

咱们画一个内存图,图中省略了两个String对应的instanceOopDesc实例。

不可贵出答案:

若是包括JVM中的C++实例的话,
有两个Java的String实例,
两个String实例对应的instanceOopDesc实例,
还有一个char[]数组对应的typeArrayOopDesc实例。
加一块儿一共是5个,也能够说2个String实例加上3个oop实例。

总结

String s = new String("xyz"); 建立了几个实例?

经过以上的分析,咱们会发现,每在这道题目的题干上每加一个定语,这道题目就会有不一样的答案。

是否考虑类加载过程,是否考虑JVM优化,是否包括对应的oop实例等等等等,每一个点都值得聊一聊的。

下次有人问你,你不妨把这篇的文章分享给他。

写在最后

为了写这一篇文章,我翻看了不少@RednaxelaFX前辈和周志明前辈的博客,过程当中收益良多。在这里感谢前辈们为国内JVM的科普与发展作出的贡献!

还有一个颇有趣的故事,我在查找“如何经过HSDB来了解String”相关资料的时候,看到一篇写的很好的文章,惊呼国内还有这么多低调的大神,后来添加了文章旁边的公众号,发现这个大神原来是PerfMa的创始人“寒泉子”李嘉鹏前辈,冒犯了冒犯了!

最后的最后

本人才疏学浅,文章不免会有纰漏,若是你发现了,欢迎提出,我会对其修正。

感谢您的阅读,您的点赞和留言都是对个人鼓励和支持。

若是你有什么想和我交流的,能够关注个人微信公众号“CoderW”,很是欢迎并感谢您的关注!


文中涉及代码:https://github.com/xiaoyingzhi/blog

JVM Spec Java SE 8Edition:https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

参考文章:http://isfeasible.cn/posts/view/5b84b6ab3957bb300a5bca94

参考文章:https://www.iteye.com/blog/rednaxelafx-774673

参考文章:http://lovestblog.cn/blog/2014/06/28/hsdb-string/

相关文章
相关标签/搜索