要彻底弄明白这个问题,我们需要清楚一些基本概念:Class文件中的常量池,运行时常量池(runtime constant pool),全局字符串常量池(StringTable),Java heap,一些常用字节码以及常量池中的常量类型等jvm的知识:
Class文件常量池:JVM会为我们每个类对应生成一个常量池,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。Class文件被加载之后,Class文件常量池就变成了运行时常量池。
全局字符串常量池: HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。这是个纯运行时的结构,而且是惰性(lazy)维护的。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。 注意,它只存了引用,根据这个引用可以得到具体的String对象。
Java heap:对大多数应用来说,Java heap是JVM所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用即使存放对象,几乎所有对象实例都在这里分配(随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有对象都在堆上分配变得不那么绝对了)。
概念介绍完毕,进入正题,先来看一段代码:
String str1 = new String("12");
String str2 = "12";
System.out.println(str2 == str1);
这段代码返回结果是false。
以下是字节码和常量池内容:
Constant pool:
#1 = Class #2 // CodeTest/StringTest
#2 = Utf8 CodeTest/StringTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LCodeTest/StringTest;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // java/lang/String
#17 = Utf8 java/lang/String
#18 = String #19 // 12
#19 = Utf8 12
#20 = Methodref #16.#21 // java/lang/String."<init>":(Ljava/lang/String;)V
#21 = NameAndType #5:#22 // "<init>":(Ljava/lang/String;)V
#22 = Utf8 (Ljava/lang/String;)V
#23 = Fieldref #24.#26 // java/lang/System.out:Ljava/io/PrintStream;
#24 = Class #25 // java/lang/System
#25 = Utf8 java/lang/System
#26 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Methodref #30.#32 // java/io/PrintStream.println:(Z)V
#30 = Class #31 // java/io/PrintStream
#31 = Utf8 java/io/PrintStream
#32 = NameAndType #33:#34 // println:(Z)V
#33 = Utf8 println
#34 = Utf8 (Z)V
#35 = Utf8 args
#36 = Utf8 [Ljava/lang/String;
#37 = Utf8 str1
#38 = Utf8 Ljava/lang/String;
#39 = Utf8 str2
#40 = Utf8 StackMapTable
#41 = Class #36 // "[Ljava/lang/String;"
#42 = Utf8 SourceFile
#43 = Utf8 StringTest.java
0: new #16 // class java/lang/String
3: dup
4: ldc #18 // String 12
6: invokespecial #20 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #18 // String 12
12: astore_2
13: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_2
17: aload_1
18: if_acmpne 25
21: iconst_1
22: goto 26
25: iconst_0
26: invokevirtual #29 // Method java/io/PrintStream.println:(Z)V
29: return
注意:new字节码出现几次就代表创建了多少对应实例。在JVM里,“new”字节码指令只负责把实例创建出来(包括分配空间、设定类型、所有字段设置默认值等工作),并且把指向新创建对象的引用压到操作数栈顶。此时该引用还不能直接使用,处于未初始化状态(uninitialized);如果某方法a含有代码试图通过未初始化状态的引用来调用任何实例方法,那么方法a会通不过JVM的字节码校验,从而被JVM拒绝执行。 能对未初始化状态的引用做的唯一一种事情就是通过它调用实例构造器,在Class文件层面表现为特殊初始化方法“<init>”。实际调用的指令是invokespecial,而在实际调用前要把需要的参数按顺序压到操作数栈上。在上面的字节码例子中,压参数的指令包括dup和ldc两条,分别把隐藏参数(新创建的实例的引用,对于实例构造器来说就是“this”)与显式声明的第一个实际参数("12"常量的引用)压到操作数栈上。 在构造器返回之后,新创建的实例的引用就可以正常使用了。
String str1 = new String("12"):new作为类初始化条件之一在这里出现,首先去创建一个实例,会然后调用String类型的构造器,在堆中创建一个对象,并将指向该对象的引用赋给str1。此时常量池中是存在一个UTF-8缩略编码的字符串“12”的,并且在字符串池中驻留引用。
String str2 = "12":str2指向的是常量池中的“12”。
所以结果会返回false。
我们修改一下代码:
String str1 = new String("12");
str1 = str1.intern();
String str2 = "12";
System.out.println(str2 == str1);
結果返回true,str1 = str1.intern()做了什么,根据先前结论我們可以做出以下分析:首先,str1.intern()将指向常量池中的“12”的引用赋值给str1(先前str1指向的是堆中的对象),接下来是String str2 = "12",由于常量池已经存在一个指向“12”的引用,所有以现在将该引用赋给str2,结果返回true。
好的,接下来看这两段代码:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("2");
s3.intern();
String s4 = "12";
System.out.println(s3 == s4);
它们的结果分别为false和true。
第一段代码前面已经解释过,看第二段:
一样的,要先用ldc把"1"和"2"送到栈顶,换句话说,会创建他俩的对象,并且会保存引用到字符串常量池中;然后有个+号对吧,内部是创建了一个StringBuilder对象,一路append,最后调用StringBuilder对象的toString方法得到一个String对象(内容是12,注意这个toString方法会new一个String对象),并把它赋值给s3。注意啊,没有把12的引用放入字符串常量池。接下来intern方法一看,字符串常量池里面没有,它会把上面的这个hello对象的引用保存到字符串常量池,然后返回这个引用,但是这个返回值我们并没有使用变量去接收,所以没用,并且此时常量池中不存在“12”字符串,JDK1.6的做法是直接在常量池中生成一个 "12" 的对象。但是在JDK1.7及以上中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。String s4 = "12", 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。
参考文章:https://www.zhihu.com/question/55994121