阅读 86

String对象与字符串常量池的恩怨情仇

字符串字面量什么时候进入的字符串常量池?

首先上代码,没有什么比代码来的直接

//JDK7+  public class StringTest {     private  static String s1 = "static";     public static void main(String[] args) {         String hello1 = new String("hell") + new String("o");         String hello2 = new String("he") + new String("llo");         String hello3 = hello1.intern();         String hello4 = hello2.intern();         System.out.println(hello1 == hello3); // true         System.out.println(hello1 == hello4); // true     } } 复制代码

下边是通过javap -verbose命令反编译Class文件后的内容(Constant pool就是Class文件常量池的内容),先在这里放着,后边会用到。

Classfile /E:/workspace/VariousCases/target/classes/cn/onenine/jvm/constantpool/StringTest.class   Last modified 2021-8-3; size 1299 bytes   MD5 checksum 338bd0034155ec3bf8d608540a31761c   Compiled from "StringTest.java" public class cn.onenine.jvm.constantpool.StringTest   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool:    #1 = Class              #2             // cn/onenine/jvm/constantpool/StringTest    #2 = Utf8               cn/onenine/jvm/constantpool/StringTest    #3 = Class              #4             // java/lang/Object    #4 = Utf8               java/lang/Object    #5 = Utf8               s1    #6 = Utf8               Ljava/lang/String;    #7 = Utf8               <clinit>    #8 = Utf8               ()V    #9 = Utf8               Code   #10 = String             #11            // static      #11 = Utf8               static   #12 = Fieldref           #1.#13         // cn/onenine/jvm/constantpool/StringTest.s1:Ljava/lang/String;   #13 = NameAndType        #5:#6          // s1:Ljava/lang/String;   #14 = Utf8               LineNumberTable   #15 = Utf8               LocalVariableTable   #16 = Utf8               <init>   #17 = Methodref          #3.#18         // java/lang/Object."<init>":()V   #18 = NameAndType        #16:#8         // "<init>":()V   #19 = Utf8               this   #20 = Utf8               Lcn/onenine/jvm/constantpool/StringTest;   #21 = Utf8               main   #22 = Utf8               ([Ljava/lang/String;)V   #23 = Class              #24            // java/lang/StringBuilder   #24 = Utf8               java/lang/StringBuilder   #25 = Class              #26            // java/lang/String   #26 = Utf8               java/lang/String   #27 = String             #28            // hell   #28 = Utf8               hell   #29 = Methodref          #25.#30        // java/lang/String."<init>":(Ljava/lang/String;)V   #30 = NameAndType        #16:#31        // "<init>":(Ljava/lang/String;)V   #31 = Utf8               (Ljava/lang/String;)V   #32 = Methodref          #25.#33        // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;   #33 = NameAndType        #34:#35        // valueOf:(Ljava/lang/Object;)Ljava/lang/String;   #34 = Utf8               valueOf   #35 = Utf8               (Ljava/lang/Object;)Ljava/lang/String;   #36 = Methodref          #23.#30        // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V   #37 = String             #38            // o   #38 = Utf8               o   #39 = Methodref          #23.#40        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   #40 = NameAndType        #41:#42        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   #41 = Utf8               append   #42 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;   #43 = Methodref          #23.#44        // java/lang/StringBuilder.toString:()Ljava/lang/String;   #44 = NameAndType        #45:#46        // toString:()Ljava/lang/String;   #45 = Utf8               toString   #46 = Utf8               ()Ljava/lang/String;   #47 = String             #48            // he   #48 = Utf8               he   #49 = String             #50            // llo   #50 = Utf8               llo   #51 = Methodref          #25.#52        // java/lang/String.intern:()Ljava/lang/String;   #52 = NameAndType        #53:#46        // intern:()Ljava/lang/String;   #53 = Utf8               intern   #54 = Fieldref           #55.#57        // java/lang/System.out:Ljava/io/PrintStream;   #55 = Class              #56            // java/lang/System   #56 = Utf8               java/lang/System   #57 = NameAndType        #58:#59        // out:Ljava/io/PrintStream;   #58 = Utf8               out   #59 = Utf8               Ljava/io/PrintStream;   #60 = Methodref          #61.#63        // java/io/PrintStream.println:(Z)V   #61 = Class              #62            // java/io/PrintStream   #62 = Utf8               java/io/PrintStream   #63 = NameAndType        #64:#65        // println:(Z)V   #64 = Utf8               println   #65 = Utf8               (Z)V   #66 = Utf8               args   #67 = Utf8               [Ljava/lang/String;   #68 = Utf8               hello1   #69 = Utf8               hello2   #70 = Utf8               hello3   #71 = Utf8               hello4   #72 = Utf8               StackMapTable   #73 = Class              #67            // "[Ljava/lang/String;"   #74 = Utf8               SourceFile   #75 = Utf8               StringTest.java {   static {};     descriptor: ()V     flags: ACC_STATIC     Code:       stack=1, locals=0, args_size=0          0: ldc           #10                 // String static          2: putstatic     #12                 // Field s1:Ljava/lang/String;          5: return       LineNumberTable:         line 11: 0       LocalVariableTable:         Start  Length  Slot  Name   Signature   public cn.onenine.jvm.constantpool.StringTest();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=1, args_size=1          0: aload_0          1: invokespecial #17                 // Method java/lang/Object."<init>":()V          4: return       LineNumberTable:         line 10: 0       LocalVariableTable:         Start  Length  Slot  Name   Signature             0       5     0  this   Lcn/onenine/jvm/constantpool/StringTest;   public static void main(java.lang.String[]);     descriptor: ([Ljava/lang/String;)V     flags: ACC_PUBLIC, ACC_STATIC     Code:       stack=5, locals=5, args_size=1          0: new           #23                 // class java/lang/StringBuilder          3: dup          4: new           #25                 // class java/lang/String          7: dup          8: ldc           #27                 // String hell         10: invokespecial #29                 // Method java/lang/String."<init>":(Ljava/lang/String;)V         13: invokestatic  #32                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;         16: invokespecial #36                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V         19: new           #25                 // class java/lang/String         22: dup         23: ldc           #37                 // String o         25: invokespecial #29                 // Method java/lang/String."<init>":(Ljava/lang/String;)V         28: invokevirtual #39                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;         31: invokevirtual #43                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;         34: astore_1         35: new           #23                 // class java/lang/StringBuilder         38: dup         39: new           #25                 // class java/lang/String         42: dup         43: ldc           #47                 // String he         45: invokespecial #29                 // Method java/lang/String."<init>":(Ljava/lang/String;)V         48: invokestatic  #32                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;         51: invokespecial #36                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V         54: new           #25                 // class java/lang/String         57: dup         58: ldc           #49                 // String llo         60: invokespecial #29                 // Method java/lang/String."<init>":(Ljava/lang/String;)V         63: invokevirtual #39                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;         66: invokevirtual #43                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;         69: astore_2         70: aload_1         71: invokevirtual #51                 // Method java/lang/String.intern:()Ljava/lang/String;         74: astore_3         75: aload_2         76: invokevirtual #51                 // Method java/lang/String.intern:()Ljava/lang/String;         79: astore        4         81: getstatic     #54                 // Field java/lang/System.out:Ljava/io/PrintStream;         84: aload_1         85: aload_3         86: if_acmpne     93         89: iconst_1         90: goto          94         93: iconst_0         94: invokevirtual #60                 // Method java/io/PrintStream.println:(Z)V         97: getstatic     #54                 // Field java/lang/System.out:Ljava/io/PrintStream;        100: aload_1        101: aload         4        103: if_acmpne     110        106: iconst_1        107: goto          111        110: iconst_0        111: invokevirtual #60                 // Method java/io/PrintStream.println:(Z)V        114: return       LineNumberTable:         line 13: 0         line 14: 35         line 15: 70         line 16: 75         line 17: 81         line 18: 97         line 20: 114       LocalVariableTable:         Start  Length  Slot  Name   Signature             0     115     0  args   [Ljava/lang/String;            35      80     1 hello1   Ljava/lang/String;            70      45     2 hello2   Ljava/lang/String;            75      40     3 hello3   Ljava/lang/String;            81      34     4 hello4   Ljava/lang/String;       StackMapTable: number_of_entries = 4         frame_type = 255 /* full_frame */           offset_delta = 93           locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]           stack = [ class java/io/PrintStream ]         frame_type = 255 /* full_frame */           offset_delta = 0           locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]           stack = [ class java/io/PrintStream, int ]         frame_type = 79 /* same_locals_1_stack_item */           stack = [ class java/io/PrintStream ]         frame_type = 255 /* full_frame */           offset_delta = 0           locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]           stack = [ class java/io/PrintStream, int ] } SourceFile: "StringTest.java" 复制代码


在类加载阶段,JVM不会立即在堆中创建这些Class文件常量池中的字符串对象实例,并把创建完对象的堆中的引用放到字符串常量池中,而是在类的Resolve阶段执行,并且JVM规范里明确指定Resolve阶段可以是Lazy的。

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

  1. CONSTANT_Utf8

  2. CONSTANT_String


后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。查看上边通过javap反编译的内容也可以验证此结论, #10对应的是静态常量指向的**#11的"static"字符串常量引用

在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。
上边反编译的内容中也可以看到在112行中有ldc命令:   8: ldc           #27                 // String hell

因此得出结论**:HotSpot中,加载类的时候字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(也就是String table中没有该字符串对象的引用,在堆中也没有相应的对象产生)

“ldc”命令是个啥?

大体意思就是:将int,float或String型常量值从常量池中推送至栈顶

在上边的反编译代码中第112行 8: ldc           #27                 // String hell的意思就是将"hell"推送至栈顶。
上边已经讲了字符串常量是lazy resolve的,那怎么推呢?是不是得有一个契机把对象创建,把对象的引用放到字符串常量池里去?****其实就是ldc指令,这个指令就是触发lazy resolve动作的条件。

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

也就是说,ldc指令是否会创建新的String实例,全看在第一次执行ldc指令时String table是否已经记录过同样内容的引用。

运行过程解析

解析之前首先我们先要了解String#intern到底做了什么

  • JDK6中,intern方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串引用。

  • JDK7及以后,intern方法实现就不需要再拷贝字符串的实例到永久代了,首先会判断常量池中是否已经有这个字符串了,如果有则直接返回常量池中它的引用,否则就将它的引用保存一份到字符串常量池中,然后返回这个引用,是返回引用,字符串常量池也就是String Table,这在JVM内存模型中已经讲过。

了解完String#intern之后,开始分析上边代码的运行过程,为了方便,我们把上边的代码复制过来:

//JDK7+  public class StringTest {     private  static String s1 = "static";(1)     public static void main(String[] args) {         String hello1 = new String("hell") + new String("o"); (2)         String hello2 = new String("he") + new String("llo");(3)         String hello3 = hello1.intern();(4)         String hello4 = hello2.intern();(5)         System.out.println(hello1 == hello3); // true (6)         System.out.println(hello1 == hello4); // true (7)     } } 复制代码

(1):static变量s1,指向"static"字符串对象引用,查看反编译代码可查看也是通过ldc指令将"static"推到栈顶(创建字符串实例,将实例引用放到String table),然后通过putstatic指令赋值。
在这里插入图片描述
(2):查看反编译代码可知,会将"hello"和"o"通过ldc指令推到栈顶,还可以看到针对“+”号,编译器会创建StringBuilder对象并通过append方法将字符串拼接起来,并通过astore_1指令将拼接完的字符串赋值给第二个局部变量,重点:这里没有ldc命令将拼接后的字符串"hello"推到栈顶,也就是说没有将"hello"的引用放入String table
在这里插入图片描述
(3):hello1调用了intern方法,hello1变量持有的是“hello”实例在堆中的引用,由于(2)没有将“hello”放入全局字符串常量池,因此(3)在调用inern方法时会将“hello”实例对象的引用放入String table,然后返回引用给hello3,因此这里hello3和hello1持有的引用是同一个(都是“hello”实例在堆中的地址)
(4):第五句hello2调用了intern方法,hello2持有的引用为“hello”实例在堆中的引用(与hello1持有的引用不同,虽然都是“hello”的实例),hello2调用intern方法时查找String table中发现有“hello”的引用(hello1持有的“hello”实例引用),就直接返回了String table中的引用并赋值给hello4。
**因此hello4、hello3和hello1持有的引用是同一个,hello2持有的引用与它们都不同。**通过一张图表示如下:
在这里插入图片描述 下边这张图用来表示下局部变量表、堆、字符串常量池之间的关系
在这里插入图片描述


作者:零壹玖
链接:https://juejin.cn/post/7026337647282880543

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐