关于java:String-s-new-Stringxyz创建了几个实例你真的能答对吗

6次阅读

共计 4709 个字符,预计需要花费 12 分钟才能阅读完成。

从面试题说起

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

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

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

这个所谓的“标准答案”槽点太多,前面咱们缓缓剖析。

然而我感觉这个问题自身不具备什么意义,因为他没有既定义“创立”的具体含意,又没有指定“创立”的工夫,是运行时吗?包不包含类加载的时候?有没有上下文代码语境?也没有定义实例是指什么实例,是指 Java 实例吗?还是单指 String 实例?包不包含 JVM 中的 C ++ 实例?

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

String 构造

在剖析之前,为了不便前面画内存图,咱们须要对 Java 中的 String 构造有一个大抵理解:

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

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

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

serialVersionUID:序列化用的。

失常的问题与正当的解释

在下面的题干上加上一些限定词,能够失去一个新的问题:

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 类型的变量:

  1. s1,指向上面 String 实例的 1
  2. s2,指向与 s1 雷同
  3. s3,值为 null,不指向任何实例

以及 3 个 String 实例:

  1. “xyz” 字面量对应的驻留的字符串常量的 String 实例
  2. “” 字面量对应的驻留的字符串常量的 String 实例(String.concat()是个乏味的办法,当发现传入的参数是空字符串时会返回 this,所以这里不会额定创立新的 String 实例)
  3. 通过 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 的创始人“寒泉子”李嘉鹏前辈,触犯了触犯了!

最初的最初

本着对每一篇收回去的文章负责的准则,文中波及常识实践,我都会尽量在官网文档和权威书籍找到并加以验证。但即便这样,我也不能保障文中每个点都是正确的,如果你发现错误之处,欢送指出,我会对其修改。

创作不易,你的正反馈对我来说十分重要!点个赞,点个再看,点个关注甚至评论区发送一条 666 都是对我最大的反对!

我是 CoderW,一个一般的程序员。

谢谢你的浏览,咱们下期再见

参考文章

文中波及代码:https://github.com/xiaoyingzh…

JVM Spec Java SE 8Edition:https://docs.oracle.com/javas…

参考文章:http://isfeasible.cn/posts/vi…

参考文章:https://www.iteye.com/blog/re…

参考文章:http://lovestblog.cn/blog/201…

正文完
 0