• JVM中字符串的秘密


    简介

    字符数组的存储方式

    字符串常量池

    字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化,在Permanent Generation中专门有一块区域用来存储字符串常量池(一组指针指向Heap中的String对象的内存地址)。

    在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个HashTable,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

    • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
    • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。关于String在内存中的存储和String#intern()方法的说明。

    字符串Hashcode

    不通方式创建字符串在JVM存储的形式

    • 双引号方式

    双引号引起来的字符串,首先从常量池中查找是否存在此字符串。如果不存在,则在常量池中添加此字符串。在堆中创建字符串对象,因String底层是通过char数组形式存储的,所以同时会在堆中生成一个TypeArrayOopDesc用来存储char数组对象。如果存在,则直接引用此字符串对象。

    测试代码1: 

    public static  void test1(){
            String s1="11";
            String s2="11";
    
            System.out.println(s1==s2);
        }

    测试结果:

      原因分析:

    s1代码执行后,常量池中添加了“11”这个常量,在堆中也创建了String对象并引用此常量的。当s2代码执行时,先在常量池中查找是否存在“11”这个常量,发现常量池中存在这个值,就找到引用此常量的字符串对象,将s2的引用指向找到的字符串对象。因为s1和s2指向同一个地址,所以比较结果为true。    

    • new String

    1、首先从常量池中查找是否存在括号内的常量,如果不存在,则在常量池中添加此字符串。在堆中创建字符串对象,因String底层是通过char数组形式存储的,所以同时会在堆中生成一个TypeArrayOopDesc用来存储char数组对象。如果存在,则直接引用堆中存在的字符串对象。

    2、通过new方式创建的String对象,每次都会在Heap上创建一个新的实例。并将此新实例中char数组对象,指向第一步堆中的已经存在的TypeArrayOopDesc。

    测试代码:

    public static void test2() {
            String s1 = new String("11");
            String s2 = new String("11");
    
            System.out.println(s1 == s2);
        }

     测试结果:

     原因分析:

    通过new方式创建的String对象,每次都会在Heap上创建一个新的实例。所以s1和s2的分别指向了不同的实例,引用地址不同。

    测试代码:

     public static void test3() {
            String s1 = new String("11");
            String s2 = "11";
    
            System.out.println(s1 == s2);
        }

     测试结果:

     原因分析:

    当执行s1时,首先会将括号内的字面量常量“11”添加到常量池中,并且在堆中生成字符串实例及char数组实例TypeArrayOopDesc。再通过new方式创建的String对象,会在Heap上新创建一个实例,此新实例中char数组不需要新的实例,指向堆中的已存在的TypeArrayOopDesc。

    当执行s2时,在常量池中发现常量已存在,则直接将虚拟机栈的指向堆中代表此常量的字符串实例。

    因此s1和s2的分别指向了不同的实例,引用地址不同。

     【缺图】

    字符串在JVM中是如何拼接的

    测试代码:

     public static void test4(){
            String s2="1"+"1";
            String s1="11";
    
    
            System.out.println(s1==s2);
        }

     测试结果:

      

      原因分析:

     文件在编译期成字节码时,编译器将“1”+“1”变成了“11”,编译后,相当于s2="11"。就与上面的测试代码1相同了,具体原因见测试代码1的原因分析。

    测试代码:

      public static void test5(){
            String s1="1";
            String s2="1";
            String s3=s1+s2;
            String s4="11";
    
            System.out.println(s3==s4);
        }

     测试结果:

      原因分析:

    编译器在编译时无法确定s3的值,是在运行时才能确定,保存在jvm的堆里面,在拼接的时候,先在常量池里面生成是s1、s2的字符串,在执行加号的时候,会从常量池中取出s1、s2常量,在堆中生成两个字符串对象,然后再生成第三个字符串对象来保存两个对象拼接后的值。

     

    测试代码:

     public static void test6() {
            final String s1 = "1";
            final String s2 = "1";
            String s3 = s1 + s2;
            String s4 = "11";
    
            System.out.println(s3 == s4);
        }

     测试结果:

     原因分析:

    通过s1、s2增加final修饰符,s1和s2的值赋值后不允许改变,这样编译器在编译时会把s3编译成s3="11",所以在执行时会字符串常量池中添加“11”这个常量,执行s4时会在常量池中找到“11”这个常量, s4会执行堆中已存在的字符串对象。因此s3和s4相等。 

    intern做了什么

    intern()方法:

    public String intern()

    JDK源代码如下图:

    返回字符串对象的规范化表示形式。

    一个初始时为空的字符串池,它由类 String 私有地维护。

    当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),

    则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。

    它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

    所有字面值字符串和字符串赋值表达式都是内部的。

    返回:

    一个字符串,内容与此字符串相同,但它保证来自字符串池中。

    尽管在输出中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。

    在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,

    如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。

    测试代码:

    public static void test8_3(){
            String s1="11";
            String s2=new String("11");
            String s3=s2.intern();
    
            System.out.println(s1==s2);//#1
            System.out.println(s1==s3);//#2
        }

     测试结果:

     原因分析:

    结果 #1:因为s1指向的是字符串中的常量,s2是在堆中生成的对象,所以s1==s2返回false。

    结果 #2:s2调用intern方法,会将s2中值(“string”)复制到常量池中,但是常量池中已经存在该字符串(即s1指向的字符串),

    所以直接返回该字符串的引用,因此s1==s2返回true。

    测试代码:

        public static void test8_4(){
            String s1="1";
            final String s2="1";
            String s3="11";
            String s4="1"+"1";
            String s5=s1+"1";
            String s6=s2+"1";
            String s7=new String("11").toString().intern();
    
            System.out.println(s3==s4);//#1
            System.out.println(s3==s5);//#2
            System.out.println(s3==s6);//#3
            System.out.println(s3==s7);//#4
    
        }

     测试结果:

     

     原因分析:

    通过反编译文件,比较容易理解:

    在解释上述执行过程之前,先了解两条指令:

    ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。

    astore_<n>:Store reference into local variable,将引用赋值给第n个局部变量。

    现在我们开始解释字节码的执行过程:

    0: ldc     #8 :加载常量池中的第八项(“1”)到栈中。

    2: astore_0    :将1中的引用复制给第零个局部变量,即  String s1="1";

    3: ldc     #8 :加载常量池中的第八项(“1”)到栈中。

    5: astore_1    :将3中的引用赋值给第一个局部变量,即 final String s2="1";

    6: ldc     #3 :加载常量池中的第三项(“11”)到栈中。

    8: astore_2    :将6中的引用赋值给第二个局部变量,即 String s3="11";

    9: ldc     #3 :加载常量池中的第三项(“11”)到栈中。

    11: astore_3    :将9中的引用赋值给第三个局部变量,即 String s4="11";

    结果#1:s3==s4 肯定会返回true,因为s3和s4都指向常量池中的同一引用地址。

    其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。

    12: new           #9:生成StringBuilder的实例。

    15: dup      :赋值12生成对象的引用并压入栈中。

    16: invokespecial #10: 滴啊用常量池中的第十项,即StringBuilder.<init>方法。

    以上三条指令的作用是生成一个StringBuilder的对象。

    19: aload_0    :加载第零个局部变量的值,即“1”

    20: invokevirtual #11 : 调用StringBuilder对象的append方法。

    23: ldc           #8 :加载常量池中第八项(“1”)到栈中。

    25: invokevirtual #11 :调用StringBuilder对象的append方法。

    28: invokevirtual #12 :调用StringBuilder对象的toString方法。

    31: astore 4    :将28中的结果引用赋值给第四个局部变量,即对变量s5进行赋值。

    结果 #2:因为s5实际上是stringBuilder.append()生成的结果,所以与s3不相等,结果返回false。     

    33: ldc           #3:加载常量池中第三项(“11”) 到栈中。

    35: astore 5    :将33中的引用赋值给第五个局部变量,即s6=“11”。

    结果 #3 :因为s3和s6指向的都是常量池中相同的引用,所以s3==s6返回true。

    这里我们还能发现一个现象,对于加了final属性的字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。

    37: new           #6 :创建String对象。

    40: dup      :复制引用并压如栈中。

    41: ldc      #3:加载常量池中的第三项(“11”)到栈中。

    43: invokespecial #7 :调用String.”<init>”方法,并传42步骤中的引用作为参数传入该方法。

    46: invokevirtual #20 :调用String.tostring()方法。

    49: invokevirtual #13 :调用String.intern方法。

    从37到49的对应的源码就是new String("11").toString().intern();

    52: astore 6  :将49步返回的结果赋值给变量6,即s7指向11在常量池中的位置。

    结果 #6 :因为s7和str3都指向的都是常量池中的同一个字符串,所以s3==s7返回true。

    测试代码:

        public static void test8_5_1(){
            String s1=new String("1")+new String("1");
            s1.intern();
            String s2="11";
            System.out.println(s1==s2);//#1
        }
    
        public static void test8_5_2(){
            String s2="11";
            String s1=new String("1")+new String("1");
            s1.intern();
            System.out.println(s1==s2);//#2
        }

     测试结果:

     原因分析:

    JDK 1.7后,对于第一种情况返回true,但是调换了一下位置返回的结果就变成了false。这个原因主要是从JDK 1.7后,

    HotSpot 将常量池从永久代移到了元空间,正因为如此,JDK 1.7 后的intern方法在实现上发生了比较大的改变,

    JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,

    如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。所以:

    结果 #1:在第一种情况下,因为常量池中没有“11”这个字符串,所以会在常量池中生成一个对堆中的“11”的引用,

    而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此s1和s2都指向堆中的字符串,返回true。

    结果 #2:调换位置以后,因为在进行字面量赋值(String s2 = “11″)的时候,常量池中不存在,所以s2指向的常量池中的位置,

    而s1指向的是堆中的对象,再进行intern方法时,对s1和s2已经没有影响了,所以返回false。

    测试代码:

      public static void test8_6_1(){
            String s1=new StringBuilder("1").append("1").toString();
            System.out.println(s1==s1.intern());//#1
        }
    
        public static void test8_6_2(){
            String s1=new StringBuilder("11").toString();
            System.out.println(s1==s1.intern());//#2
        }

     测试结果:

     原因分析:

    结果#1 :

    String s1 = new StringBuilder("1").append("1").toString();
    System.out.println(s1==s1.intern());
    上面的代码等价于下面的代码
    String a = "1";
    String b = "1";
    String str3 = new StringBuilder(a).append(b).toString();
    System.out.println(s1==s1.intern());
    很容易分析出:
    “11” 最先创建在堆中 s1.intern()然后缓存在字符串常连池中 运行结果为true.

    结果#2:

    String s1 = new StringBuilder("11").toString();
    System.out.println(s1==s1.intern());
    可以写成下面的形式
    String a = "11";
    String s1 = new StringBuilder(a).toString();
    System.out.println(s1==s1.intern());

    很容易分析出:
    “11” 最先创建在常量池中, 运行结果为false.

  • 相关阅读:
    第一阶段SCRUM冲刺-06
    第一阶段SCRUM冲刺-05
    第一阶段SCRUM冲刺-04
    第一阶段SCRUM冲刺-03
    第一阶段SCRUM冲刺-02
    第一阶段SCRUM冲刺-01
    19年春第十周学习
    个人作业8-单词统计
    19年春第九周学习
    个人作业6-用户模板和用户场景
  • 原文地址:https://www.cnblogs.com/gengaixue/p/13493522.html
Copyright © 2020-2023  润新知