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

原问题是关于 intern方法的。 在看了很多博文以后,我总结出了我所真正困惑的关键所在:字面量进入字符串常量池的时机。 对于代码: class Ne…
关注者
193
被浏览
90,409

14 个回答

受R大委托更新回答。


都有哪些常量池?

1.Class文件中的常量池

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

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

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

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

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

III.方法的名称和描述符

这个用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是可以重新指向的对吧。

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

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

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

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

JVM规范里Class文件的常量池项的类型,有两种东西:

1.CONSTANT_Utf8

2.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文件如下

使用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的实现,考虑题主给出的第一段代码:

// JDK1.7
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",它会把上面的这个hello对象的引用保存到字符串常量池,然后返回这个引用,但是这个返回值我们并没有使用变量去接收,所以没用。

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

第五句,已经很明显了。

再看另外一段代码:

// JDK1.7
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"对象。

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

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

很显然,题主应该是Jdk7的环境下执行的上述验证代码;


如果在JDK6的环境下执行上述代码,得到的结果应该是false false false


------------------

只需要结合以下几个结论就能解释题主以及这两段代码在JDK6和JDK7下的不同表现


  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
  • 如果是new 出来的String对象,将会在堆上直接创建对象
  • JDK6中的intern方法,由于堆中的字符串常量池pool在Perm中,与堆中其他对象是分开的,于是乎如果一个只存在于堆中的String对象调用这个方法,将会在string pool当中重新创建该对象,并返回其引用;但是在JDK7中Perm的概念不存在了,该方法将直接在常量池中保存堆中的引用,不会创建新的对象

想详细了解该部分知识的可以参看