要彻底弄明白这个问题,我们需要清楚一些基本概念:
- Class文件中的常量池
- 运行时常量池(runtime constant pool)
- 全局字符串池(StringTable)
- Java heap
- 一些常用字节码以及常量池中的常量类型等 jvm 的知识
Class 文件常量池:JVM 会为我们每个类对应生成一个常量池,常量池可以理解为 Class 文件之中的资源仓库,它是 Class文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同时它还是在 Class 文件中第一个出现的表类型数据项目。Class 文件被加载之后,Class 文件常量池就变成了运行时常量池。
全局字符串池:全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
Java heap:对大多数应用来说,Java heap 是 JVM 所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用即使存放对象,几乎所有对象实例都在这里分配(随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有对象都在堆上分配变得不那么绝对了)。
概念介绍完毕,进入正题,先来看一段代码:
String str1 = new String("12");
String str2 = "12";
System.out.println(str2 == str1);
这段代码创建了几个String实例?
结论是两个,如果单独来看的话 String str1 = new String("12");会创建两个,String str2 = "12"只创建一个
![]()
![]()
创建了哪些对象呢,下面通过常量池来分析
这段代码返回结果是 false。
以下是常量池和字节码内容:
为了避免混淆,str1 和 str2 分开来查看常量池和字节码
String str1 = new String("12"):
- 0:new : 创建一个对象,并将其引用值压入栈顶,对应常量池中的第16行。
- 3:dup : 复制栈顶数值并将复制值压入栈顶。
- 4:ldc : 将 int , float 或 String 型常量值从常量池中推送至栈顶,对应常量池中的第18行。
- 6:invokespecial : 调用超类构造方法,实例初始化方法,私有方法,对应常量池中的第20行。
- 9:astore_1 : 将栈顶引用型数值存入第二个本地变量,这里对应 LocalVariableTable 中的 string1。
- 10:return : 从当前方法返回 void。
我们可以看到new 的方式创建字符串会创建两个实例 ,他们分别使用字节码指令new创建出来的string对象和从常量池ldc到栈顶的字面量
String str2 = "12":
- 0:ldc : 将 int , float 或 String 型常量值从常量池中推送至栈顶,对应常量池中的第16行。
- 2:astore_1 : 将栈顶引用型数值存入第二个本地变量,这里对应 LocalVariableTable 中的 string2。
- 3:return : 从当前方法返回 void。
我们可以看到直接以字面量创建字符串的话只会有一个ldc上去的字面量
注意:new 字节码出现几次就代表创建了多少对应实例。在 JVM 里,"new" 字节码指令只负责把实例创建出来(包括分配空间、设定类型、所有字段设置默认值等工作),并且把指向新创建对象的引用压到操作数栈顶。此时该引用还不能直接使用,处于未初始化状态(uninitialized);如果某方法 a 含有代码试图通过未初始化状态的引用来调用任何实例方法,那么方法a会通不过 JVM 的字节码校验,从而被 JVM 拒绝执行。 能对未初始化状态的引用做的唯一一种事情就是通过它调用实例构造器,在 Class 文件层面表现为特殊初始化方法 "<init>"。实际调用的指令是 invokespecial,而在实际调用前要把需要的参数按顺序压到操作数栈上。在上面的字节码例子中,压参数的指令包括dup和ldc两条,分别把隐藏参数(新创建的实例的引用,对于实例构造器来说就是“this”)与显式声明的第一个实际参数("12"常量的引用)压到操作数栈上。 在构造器返回之后,新创建的实例的引用就可以正常使用了。
String str1 = new String("12"):new 作为类初始化条件之一在这里出现,首先去创建一个实例,会然后调用 String 类型的构造器,在堆中创建一个对象,并将指向该对象的引用赋给 str1,并会在StringTable中驻留引用。所以后面的String str2 = "12"才没有创建对象
但是,此时StringTable中的"12"实际上是指向常量池中的符号引用"12"resolve之后的实例的引用,并不是指向堆中的那个string
String str2 = "12":str2指向的是StringTable中的 "12"。
所以结果会返回false。
我们修改一下代码:
String str1 = new String("12");
str1 = str1.intern();
String str2 = "12";
System.out.println(str2 == str1);
如果StringTable中已经有了这个字符串,那么直接返回StringTable中它的引用,如果没有,那就将它的引用保存一份到StringTable,然后直接返回这个引用。敲黑板,这个方法是有返回值的,是返回引用。。
結果返回true,str1 = str1.intern() 做了什么,根据先前结论我們可以做出以下分析:首先,str1.intern()先去StringTable查看有没有"12",有, 将指向StringTable中的 "12" 的引用赋值给 str1,接下来是 String str2 = "12",由于StringTable已经存在一个指向 "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 。
第一段代码前面已经解释过,看第二段:
- 0:new : 创建一个 StringBuilder 对象,并将其引用值压入栈顶。
- 3:dup : 复制栈顶数值并将复制值压入栈顶,这时是 StringBuilder 对象的引用。
- 4:new : 创建一个 String 对象,并将其引用值压入栈顶。
- 7:dup : 复制栈顶数值并将复制值压入栈顶,这时是 String 对象的引用。
- 8:ldc : 将 int , float 或 String 型常量值从常量池中推送至栈顶,在这里是 String s3 = new String("1") + new String("2") 中的 "1" 。
- 10:invokespecial : 调用 String 类的构造方法来进行初始化。
- 13:invokestatic : 调用 String 类的 valueOf 方法,这是个静态方法。
- 16:invokespecial : 调用 StringBuilder 类的构造方法来进行初始化。
- 19:new : 创建一个 String 对象,并将其引用值压入栈顶。
- 22:dup : 复制栈顶数值并将复制值压入栈顶,这时是 String 对象的引用。
- 23:ldc : 将 int , float 或 String 型常量值从常量池中推送至栈顶,在这里是 String s3 = new String("1") + new String("2") 中的 "2" 。
- 25:invokespecial : 调用 String 类的构造方法来进行初始化。
- 28:invokevirtual : 调用 StringBuild.append() 实例方法。
- 31:invokevirtual : 调用 StringBuilder.toString() 实例方法。
- 34:astore_1 : 将栈顶引用型数值存入第二个本地变量,这里对应 LocalVariableTable 中的 s3。
- 35:aload_1 : 将第二个引用类型本地变量推送至栈顶。
- 36:invokevirtual : 调用 StringBuilder.intern() 实例方法。
- 39:pop : 将栈顶数值弹出。
- 40:ldc : 将 int , float 或 String 型常量值从常量池中推送至栈顶,在这里是 "12"。
- 42:astore_2 : 将栈顶引用型数值存入第三个本地变量,这里对应 LocalVariableTable 中的 s4。
一样的,要先用 ldc 把 "1" 和 "2" 送到栈顶,换句话说,会创建他俩的对象,并且会保存引用到StringTable中;然后有个+号对吧,内部是创建了一个 StringBuilder 对象,一路 append ,最后调用 StringBuilder 对象的 toString 方法得到一个 String 对象(内容是12,注意这个 toString 方法会 new 一个 String 对象),并把它赋值给 s3。注意啊,没有把 这个String实例 的引用放入StringTable,常量池里也没有哦。接下来 intern 方法一看,StringTable里面没有,它会把上面的这个s对象的引用保存到StringTable,然后返回这个引用,但是这个返回值我们并没有使用变量去接收,所以没用,此时StringTable中已经有指向堆中"12"的引用了。
========================
ldc字节码在这里的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项,如果该项尚未resolve则resolve之,并返回resolve后的内容。 在遇到String类型常量时,resolve的过程如果发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,如果StringTable里尚未有内容匹配的String实例的引用,则会在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用出去。
========================
所以s4和s3都指向的是堆中的那个"12"
因此 s3 == s4 返回了 true 。
同理:
String a = "12"; String b = "12"; System.out.print(a == b);//true 在堆中一个对象
String a = new String("12"); String b = new String("12"); System.out.print(a == b);//false 在堆中两个对象
再看看这个:
得,官方文档都推荐我们直接赋值而不是 new 了,所以我也不知道这个构造器存在的必要???
如果又不对的地方还望不吝啬赐教!
参考文章:https://www.zhihu.com/question/55994121