zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    centos 用户管理
    rsync 实验
    文件共享和传输
    PAT 1109 Group Photo
    PAT 1108 Finding Average
    PAT 1107 Social Clusters
    PAT 1106 Lowest Price in Supply Chain
    PAT 1105 Spiral Matrix
    PAT 1104 Sum of Number Segments
    PAT 1103 Integer Factorization
  • 原文地址:https://www.cnblogs.com/gengaixue/p/13493522.html
Copyright © 2011-2022 走看看