Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?

原问题是关于 intern方法的。在看了很多博文以后,我总结出了我所真正困惑的关键所在:字面量进入字符串常量池的时机。对于代码:class NewTest0 { public static String s1="static"; // 第一句 public static void main(String[] args) { String s1=new String("he")+new String("llo"); //第二句 String s2="hello"; //第三句 } } 下述仅讨论字符串。(1) NewTest.class 的 class文件常量池 中 是含有 "static" ,"he","ll…
关注者
43
被浏览
4,403

2 个回答

受R大委托更新回答。


都有哪些常量池?

1.Class文件中的常量池

这里面主要存放两大类常量:

字面量(Literal):文本字符串等

符号引用(Symbolic References):属于编译原理方面的概念,包含三类常量:

类和接口的全限定名(Full Qualified Name)

字段的名称和描述符(Descriptor)

方法的名称和描述符

这个用javap看一下就能明白,这里只涉及字符串就不谈其他的了。简单地说,用双引号引起来的字符串字面量都会进这里面。

2.运行时常量池

方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table),存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。

3.全局字符串常量池

HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。这是个纯运行时的结构,而且是惰性(lazy)维护的。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。 注意,它只存了引用,根据这个引用可以得到具体的String对象。

一般我们说一个字符串进入了全局的字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。


String的 intern 方法干了什么?

题中没有涉及JDK6就不谈了。JDK7中,如果常量池中已经有了这个字符串,那么直接返回常量池中它的引用,如果没有,那就将它的引用保存一份到字符串常量池,然后直接返回这个引用。敲黑板,这个方法是有返回值的,是返回引用。


s1.intern(); 和 s1 = s1.intern();一样吗?

差远了。然而题主给的那篇东东里面,毛看一眼似乎这俩在混用,但愿是我看花眼了。。。一个引用a,指向了一个具体的对象,然后调用了一个方法func,请问这个方法会对a本身产生什么影响吗?没有吧,换句话说,a.func(..)执行完之后,a原来指向谁还是指向谁吧,对不对?所以s1.intern();对s1有什么影响吗?一点影响都没有,原来指向哪现在还指向哪。s1 = s1.intern();就不一样了,你把intern方法的返回值给了s1,s1是可以重新指向的对吧。

<img src="pic4.zhimg.com/v2-9ded5" data-rawwidth="681" data-rawheight="430" class="content_image" width="681" data-original="pic4.zhimg.com/v2-9ded5">

字面量进入字符串常量池的时机

题主根据R大文章一开始便得出了两个结论,其中第二个是:

在类加载阶段, JVM会在堆中创建 对应这些 class文件常量池中的 字符串对象实例 并在字符串常量池中驻留其引用。具体在resolve阶段执行。这些常量全局共享。

这里说的比较笼统,没错,是resolve阶段,但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM规范里明确指定resolve阶段可以是lazy的。

JVM规范里Class文件的常量池项的类型,有两种东西: CONSTANT_Utf8 CONSTANT_String 后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池项必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。

在HotSpot VM中,运行时常量池里, CONSTANT_Utf8 -> Symbol*(一个指针,指向一个Symbol类型的C++对象,内容是跟Class文件同样格式的UTF-8编码的字符串) CONSTANT_String -> java.lang.String(一个实际的Java对象的引用,C++类型是oop)

CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。那么在尚未resolve的时候,HotSpot VM把它的类型叫做JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;等到resolve过后这个项的常量类型就会变成最终的JVM_CONSTANT_String,而内容则变成实际的那个oop。

看到这里想必也就明白了, 就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生)


ldc指令是什么东西?

简单地说,它用于将int、float或String型常量值从常量池中推送至栈顶

以下面代码为例

public class Abc { public static void main(String[] args) { String a = "AA"; } }

查看其编译后的Class文件如下

<img src="pic1.zhimg.com/v2-4d64e" data-rawwidth="531" data-rawheight="329" class="content_image" width="531" data-original="pic1.zhimg.com/v2-4d64e">

使用ldc将"AA"送到栈顶,然后用astore_1把它赋值给我们定义的局部变量a,然后就没什么事了return了。

根据上面说的,在类加载阶段,这个 resolve 阶段( constant pool resolution )是lazy的。换句话说并没有真正的对象,字符串常量池里自然也没有,那么ldc指令还怎么把人推送至栈顶?或者换一个角度想,既然resolve 阶段是lazy的,那总有一个时候它要真正的执行吧,是什么时候?

执行ldc指令就是触发这个lazy resolution动作的条件

ldc字节码在这里的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项,如果该项尚未resolve则resolve之,并返回resolve后的内容。 在遇到String类型常量时,resolve的过程如果发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,如果StringTable里尚未有内容匹配的String实例的引用,则会在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用出去。

可见,ldc指令是否需要创建新的String实例,全看在第一次执行这一条ldc指令时,StringTable是否已经记录了一个对应内容的String的引用。


这叫个什么标题好?

对于HotSpot VM的实现,考虑题主给出的第一段代码:

class NewTest1{ public static String s1="static"; // 第一句 public static void main(String[] args) { String s1=new String("he")+new String("llo"); //第二句 s1.intern(); // 第三句 String s2="hello"; //第四句 System.out.println(s1==s2);//第五句,输出是true。 } }

"static" "he" "llo" "hello"都会进入Class的常量池, 按照上面说的,类加载阶段由于resolve 阶段是lazy的,所以是不会创建实例,更不会驻留字符串常量池了。但是要注意这个“static”和其他三个不一样,它是静态的,在类加载阶段中的初始化阶段,会为静态变量指定初始值,也就是要把“static”赋值给s1(main方法里面怎么还有个s1,这里说的是外面那个静态的),这个赋值操作要怎么搞啊,先ldc指令把它放到栈顶,然后用putstatic指令完成赋值。注意,ldc指令,根据上面说的,会创建"static"字符串对象,并且会保存一个指向它的引用到字符串常量池。

额,我好像把第一句已经说了。

运行main方法后,首先是第二句,一样的,要先用ldc把"he"和"llo"送到栈顶,换句话说,会创建他俩的对象,并且会保存引用到字符串常量池中;然后有个+号对吧,内部是创建了一个StringBuilder对象,一路append,最后调用StringBuilder对象的toString方法得到一个String对象(内容是hello,注意这个toString方法会new一个String对象),并把它赋值给s1。注意啊,没有把hello的引用放入字符串常量池。

然后是第三句,intern方法一看,字符串常量池里面没有,它会把上面的这个hello对象的引用保存到字符串常量池,然后返回这个引用,但是这个返回值我们并没有使用变量去接收,所以没用。

第四句,字符串常量池里面已经有了,直接用嘛

第五句,已经很明显了。

再看另外一段代码:

class NewTest2{ public static void main(String[] args) { String s1=new String("he")+new String("llo"); // ① String s2=new String("h")+new String("ello"); // ② String s3=s1.intern(); // ③ String s4=s2.intern(); // ④ System.out.println(s1==s3); System.out.println(s1==s4); } }

类加载阶段,什么都没干。

然后运行main方法,先看第一句,会创建"he"和"llo"对象,并放入字符串常量池,然后会创建一个"hello"对象,没有放入字符串常量池,s1指向这个"hello"对象。

第二句,创建"h"和"ello"对象,并放入字符串常量池,然后会创建一个"hello"对象,没有放入字符串常量池,s2指向这个"hello"对象。

第三句,字符串常量池里面还没有,于是会把s1指向的String对象的引用放入字符串常量池(换句话说,放入池中的引用和s1指向了同一个对象),然后会把这个引用返回给了s3,所以s3==s1是true。

第四句,字符串常量池里面已经有了,直接将它返回给了s4,所以s4==s1是true。


写的我腰酸背痛的... (逃

m1 中由于 new String("1") 时引用了 "1",这时候 "1" 就已经存在于常量池了,假设这个常量地址是 a,那么 s2 地址也就是 a ,这个跟 new 出来的 s1 地址肯定不一样的