我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。

但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据

以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  1. 有人说创建了 1 个对象;
  2. 有人说创建了 2 个对象;
  3. 有人说创建了 1 个或 2 个对象。

而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。

那我们就先来说说这个「字符串常量池」。

字符串常量池

字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。

字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:



以上说法可以通过如下代码进行证明:

public class StringExample {
public static void main(String[] args) {
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2);
}
}

以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。

在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。

常量池的内存布局

JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上

JDK 1.7 内存布局如下图所示:



JDK 1.8 内存布局如下图所示:



JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:http://openjdk.java.net/jeps/122

答案解密

认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。

认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。

认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:

老王认为正确的答案:创建 1 个或者 2 个对象

技术论证

解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:

public class StringExample {
public static void main(String[] args) {
String s1 = new String("javaer-wang");
String s2 = "wang-javaer";
String s3 = "wang-javaer";
}
}

首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:

Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class
Last modified 2020年4月16日; size 401 bytes
SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e
Compiled from "StringExample.java"
public class com.example.StringExample
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #16 // com/example/StringExample
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/lang/String
#8 = Utf8 java/lang/String
#9 = String #10 // javaer-wang
#10 = Utf8 javaer-wang
#11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V
#12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V
#13 = Utf8 (Ljava/lang/String;)V
#14 = String #15 // wang-javaer
#15 = Utf8 wang-javaer
#16 = Class #17 // com/example/StringExample
#17 = Utf8 com/example/StringExample
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 SourceFile
#23 = Utf8 StringExample.java
{
public com.example.StringExample();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0 public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #7 // class java/lang/String
3: dup
4: ldc #9 // String javaer-wang
6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #14 // String wang-javaer
12: astore_2
13: ldc #14 // String wang-javaer
15: astore_3
16: return
LineNumberTable:
line 5: 0
line 6: 10
line 7: 13
line 8: 16
}
SourceFile: "StringExample.java"

备注:以上代码的运行也编译环境为 jdk1.8.0_101。

其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang");  定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String

 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。

那么问题来了,以下这段代码的执行结果为 true 还是 false?

String s1 = new String("javaer-wang");
String s2 = new String("javaer-wang");
System.out.println(s1 == s2);

既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:



从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:

public static void main(String[] args) {
String s1 = "Java";
String s2 = "Java";
String s3 = new String("Java");
String s4 = new String("Java");
System.out.println(s1 == s2);
System.out.println(s3 == s4);
}

程序执行的结果也符合预期:

true

false

扩展知识

我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:

public static void main(String[] args) {
String s1 = "abc";
String s2 = "ab" + "c";
String s3 = "a" + "b" + "c";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
}

按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。

同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:

警告: 文件 ./StringExample.class 不包含类 StringExample
Compiled from "StringExample.java"
public class com.example.StringExample {
public com.example.StringExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return public static void main(java.lang.String[]);
Code:
0: ldc #7 // String abc
2: astore_1
3: ldc #7 // String abc
5: astore_2
6: ldc #7 // String abc
8: astore_3
9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: aload_2
14: if_acmpne 21
17: iconst_1
18: goto 22
21: iconst_0
22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_1
29: aload_3
30: if_acmpne 37
33: iconst_1
34: goto 38
37: iconst_0
38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
41: return
}

从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。

总结

本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。

最后的话

原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢

别再问我 new 字符串创建了几个对象了!我来证明给你看!的更多相关文章

  1. Java设计模式(十三) 别人再问你设计模式,叫他看这篇文章

    原创文章,转载请务注明出处 OOP三大基本特性 封装 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的属性和方法只让可信的类操作,对不可信的进行信息隐藏. 继承 继承是指这样一种能力,它可以使 ...

  2. 《Java程序员面试笔试宝典》之字符串创建与存储的机制是什么

    在Java语言中,字符串起着非常重要的作用,字符串的声明与初始化主要有如下两种情况: (1)       对于String s1=new String("abc")语句与Strin ...

  3. String字符串创建与存储机制

    Java内存可以粗略的区分为堆内存(Heap)和栈内存(Stack),堆中存放的是对象实例,而栈中存放的则是方法调用过程中的局部变量或引用等. 在Java语言中,字符串的生命与初始化有如下两种方式: ...

  4. 拜托!面试请不要再问我Spring Cloud底层原理[z]

    [z]https://juejin.im/post/5be13b83f265da6116393fc7 拜托!面试请不要再问我Spring Cloud底层原理 欢迎关注微信公众号:石杉的架构笔记(id: ...

  5. 《Java程序猿面试笔试宝典》之字符串创建与存储的机制是什么

    在Java语言中.字符串起着非常关键的数据.字符串的声明与初始化主要有例如以下两种情况:(1) 对于String s1=new String("abc")语句与String s2= ...

  6. 面试官,不要再问我“Java 垃圾收集器”了

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...

  7. 面试官,不要再问我“Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

  8. Java入门系列之字符串创建方式、判断相等(一)

    前言 陆续从0开始学习Java出于多掌握一门语言以后的路也会更宽,.NET和Java兼顾,虽然路还很艰难,但事在人为.由于Java和C#语法相似,所以关于一些很基础的内容不会再重头讲,Java系列中所 ...

  9. 面试官,不要再问我“Java虚拟机类加载机制”了(转载)

    关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...

随机推荐

  1. AndroiBugs Android漏洞扫描器

    Download Address:https://github.com/AndroBugs/AndroBugs_Framework Usage for Unix/Linux: ####To run t ...

  2. RTSP协议进行视频取流的方法、注意点及python实现

    在视频应用中,我们一般都需要基于摄像头或录像机的视频流进行二次开发,那么就涉及到如何将视频流取出来. 在摄像机安装好之后,一般是通过局域网与本地的服务器进行连接,要取录像机的视频流就要在局域网范围内进 ...

  3. JDBC(三)----Spring JDBC(JDBCTemplate)

    ##  Spring  JDBC *  Spring框架对JDBC的简单封装.提供了一个JDBCTemplate对象简化JDBC的开发 1.步骤 1.导入jar包 2.创建JDBCTemplate对象 ...

  4. Oracle 11g Windows下安装出现INS-30131错误

    1.错误信息 2.解决方法 问题分析:访问Temp文件夹缺少权限 方案1: 使用net use查看C盘是否共享,否则进行设置,这种方法网上有很多帖子,不详坠 方案2:打开cmd,到相应的解压后的set ...

  5. python win32com

    要使用Python控制MS Word,您需要先安裝win32com套件,這個套件可以到 http://sourceforge.net/projects/pywin32/ 找到.本文假設您已經正確安裝w ...

  6. TensorFlow 基本变量定义,基本操作,矩阵基本操作

    使用 TensorFlow 进行基本操作的实例,这个实例主要是使用 TensorFlow 进行了加法运算. 包括使用 constant 常量进行加法运算和使用 placeholder 进行变量加法运算 ...

  7. HDU - 1166 树状数组模板(线段树也写了一遍)

    题意: 汉语题就不说题意了,用到单点修改和区间查询(树状数组和线段树都可以) 思路: 树状数组的单点查询,单点修改和区间查询. 树状数组是巧妙运用二进制的规律建树,建树就相当于单点修改.这里面用到一个 ...

  8. FME中矢量裁剪

  9. CocoaPods 安装卸载

    建议升级10.15的系统,什么都装好了 sudo gem install cocoapods pod setup搞定不能有search命令,可以pod init,下载用的是cdn,打开项目正常使用 问 ...

  10. ASP.NET Core MVC通过IViewLocationExpander扩展视图搜索路径

    IViewLocationExpander API ExpandViewLocations Razor视图路径,视图引擎会搜索该路径. PopulateValues 每次调用都会填充路由 项目目录如下 ...