从字符串到常量池,一文看懂String类设计 (4)

String#intern方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。

关于其详细的分析可以参考:美团:深入解析String#intern

珠玉在前,所以本文着重就分析下intern方法在JDK不同版本下的差异,首先我们要知道引起差异的原因是因为JDK1.7及之后将字符串常量池从永久代挪到了堆中。

我这里就以美团文章中的示例代码来进行分析,代码如下:

public static void main(String[] args) { String s = new String("1"); s.intern(); String s2 = "1"; System.out.println(s == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); }

打印结果是

jdk6 下false false

jdk7 下false true

在美团的文章中已经对这个结果做了详细的解释,接下来我就用我的图解方式再分析一波这个过程

jdk6 执行流程

第一步:执行 String s = new String("1"),要清楚这行代码的执行过程,我们还是得从字节码入手,这行代码对应的字节码如下:

public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String 1 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: return

new :创建了一个类的实例(还没有调用构造器函数),并将其引用压入操作数栈顶

dup:复制栈顶数值并将复制值压入栈顶,这是因为invokespecial跟astore_1各需要消耗一个引用

ldc:解析常量池符号引用,将实际的直接引用压入操作数栈顶

invokespecial:弹出此时栈顶的常量引用及对象引用,执行invokespecial指令,调用构造函数

astore_1:将此时操作数栈顶的元素弹出,赋值给局部变量表中1号元素(0号元素存的是main函数的参数)

我们可以将上面整个过程分为两个阶段

解析常量

调用构造函数创建对象并返回引用

在解析常量的过程中,因为该字符串常量是第一次解析,所以会先在永久代中创建一个字符串实例对象,并将其引用添加到字符串常量池中。此时内存状态如下:

image-20200617015119885

当真正通过new方式创建对象完成后,对应的内存状态如下,因为在分析class文件中的常量池的时候已经对栈区做了详细的分析,所以这里就省略一些细节了,在执行完这行代码后,栈区存在一个引用,指向 了堆区的一个字符串实例内存状态对应如下:

image-20200617015331040

第二步:紧接着,我们调用了s的intern方法,对应代码就是 s.intern()

当intern方法执行时,因为此时字符串常量池中已经存在了一个字面量信息跟s相同的字符串的引用,所以此时内存状态不会发生任何改变。

第三步:执行String s2 = "1",此时因为常量池中已经存在了字面量1的对应字符串实例的引用,所以,这里就直接返回了这个引用并且赋值给了局部变量s2。对应的内存状态如下:

image-20200617015738520

到这里就很清晰了,s跟s2指向两个不同的对象,所以s==s2肯定是false嘛~

如果看过美团那篇文章的同学可能会有些疑惑,我在图中对常量池的描述跟美团文章图中略有差异,在美团那篇文章中,直接将具体的字符串实例放到了字符串常量池中,而在我上面的图中,字符串常量池存的永远时引用,它的图是这样画的

image-20200617010744250

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zypdgs.html