StringTable

String的基本特性

  • String字符串:使用一对""引起来。
  • String声明为final的,不可被继承。
  • String实现了Serializable接口,表示字符串是支持序列化的。实现了Comparable接口:便是String是可以比较大小的。
  • String在JDK8中及以前内部定义了final char[] value用于存储字符串数据,JDK9时改为byte[].

String在JDK9中存储结构变更

更新日志

提取日志
结论:String再也不用char[] 来存储,改成用byte[]加上编码标记,节约一些空间。

   @Stable
    private final byte[] value;

那StringBuffer和StringBuilder是否仍然无动于衷呢?

String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM’s intrinsic string operations.

String的基本特性

  • String:代表不可变的字符序列。简称:不可变性。
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value赋值。
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
    • 字符串常量池中是不会存储相同内容的字符串的。
  • String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了后会直接造成的影响就是当调用String.intern时性能会大幅度下降。
  • 使用-XX:StringTableSize可设置StringTable的长度。
  • 在JDK6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符过多就会导致效率下降很快。StringTableSize设置没有要求。
  • 在JDk7中,StringTable的长度默认值是60013,StringTableSize设置没有要求
  • JDK8开始,设置StringTable的长度的话,1009是可设置的最小值。

String的内存分配

  • 在Java语言中有8中基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
  • 常量池就类似于一个Java系统级别提供的缓存。8中基本数据类型的常量池都是系统协调的。String类型的常量池比较特殊。它的主要使用方法有两种:
    • 直接使用双引号声明出来的String对象会直接存储在常量池中。
      • 比如:String info = “com.yi”;
    • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
  • 在Java 6以前,字符串常量池存放在永久代。
  • 在Java 7 中Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用的比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern().
  • Java8元空间,字符串常量在堆。
    6
    7
    8

StringTable为什么要调整?
官网
为什么要调整

String的基本操作

JAVA语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例

 public static void main(String[] args) {
        System.out.println();
        System.out.println("1");// 1731
        System.out.println("2");//1732
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");//1740


        System.out.println("1");//1741
        System.out.println("2");
        System.out.println("3");//1741
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");
    }

字符串拼接操作

  • 1、常量与常量的拼接结果在常量池,原理是编译期优化。
  • 2、常量池中不会存在相同内容的常量。
  • 3、只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。
  • 4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

编译期完成了拼接

  public void test(){
        /*
        *
        * 0 ldc #2 <12> 常量进行拼接的时候一开始就从字符串常量池中去加载 ,在编译期就完成了拼接
         2 astore_1
         3 ldc #2 <12>
         5 astore_2
        *
        * */
        String a = "1" + "2";
        String a1 = "12";
        System.out.println(a == a1);
    }

变量拼接

@Test
    public void test1(){
        String a = "javaee";
        String b = "hadoop";

        String s1 = "javaeehadoop";
        String s2 = "javaee"+"hadoop";

        /*
        *
        *   0 ldc #5 <javaee>
          2 astore_1
          3 ldc #6 <hadoop>
          5 astore_2
          6 ldc #7 <javaeehadoop>
          8 astore_3
          9 ldc #7 <javaeehadoop>
         11 astore 4
        *
        *
        *
        * */

        String b1 = a + "hadoop";
        /*
        *
     13 new #8 <java/lang/StringBuilder>
     16 dup
     17 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
     20 aload_1
     21 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
     24 ldc #6 <hadoop>
     26 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
     29 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
     32 astore 5
 * */
        String b2 = "javaee"+ b;
        String b3 = a + b;

        System.out.println(s1 == b1);
        System.out.println(s2 == b2 );
        System.out.println(s2 == b3 );
        System.out.println(b3 == b2);
        System.out.println(b3 == b1);
    }

intern()的使用

如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

  • 比如:String myinfo= new String(“i love liyi”).intern();

也就是说,如果在任意字符串上调用String.intern(),那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true

(“a”+“b”+“c”).intern() == “abc”;
通俗点将,intern String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)


  @Test
    public void test() {
        String s = new String("1");
        // 看这个字节码指令已经在字符串常量池中有了一个1 此时创建了两个对象 一个在堆中 一个在字符串常量池中。此时s的地址指向的是堆空间中的地址
        s.intern();// 这个就没有什么作用
        String s1 = "1";// 直接指向字符创常量池 地址是字符串常量池中的地址
        System.out.println(s == s1);// jdk 6 中 false
                                    // jdk7/8 中 false
    }

    public static void main(String[] args) {
        //  new String("1") + new String("1") 具体说一共创建了6个对象
        /*
        * 在字符串进行拼接的时候创建了一个StringBuilder
        * 之后创建了堆空间中的String 字符串常量池中的1 ,调用append方法进行添加,然后又创建了堆空间中的String  和 字符串常量池中的1 然后又进行append添加
        * 最后调用了StringBuilder中的toString()方法返回一个地址,观察StringBuilder中的字节码指令
        * 发现在字符串常量池中并没有创建一个11
        *
        * */

        String s = new String("1") + new String("1");
        // 此时字符串常量池中没有11 调用了intern以后会在字符串常量池中创建一个11
        /*
        在jdk6中 字符串常量池在永久代 所以地址指向不同
        在jdk7以后,字符串常量池移到了堆空间中,我们在调用intern()时候,在字符串常量池中我们
        存放的是堆空间的那个字符的地址,不会再去新创建一个字符串
         */
        s.intern();

        // 在jdk7以后直接指向字符串常量池,此时字符串常量池中存放的是堆空间中的地址,所以两个地址一样。

        String s1 = "11";
        System.out.println(s == s1);// jdk 6 中false
                                    // jdk 7/8中true
    }

intern()在jdk6和jdk7的对比

6
7

总结String.intern()使用

  • JDK6中,将这个字符串对象尝试放入串池中。
    • 如果串池中有,则不会放入,返回已有的串池中的对象的地址。
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址。
  • JDK7开始,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址。
    • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。

练习

 public static void main(String[] args) {
        String s = new String("1")+new String("2");
        String s1 = s.intern();
        System.out.println(s=="12");
        System.out.println(s1=="12");
    }

以上题目在JDK6中 结果为false true
在JDK7中,结果为 true true 。JDK7开始,将这个字符串对象尝试放入串池。如果串池中有,则并不会放入。返回已有的串池中的对象的地址。如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址。


public class Test{
    public void test(){
        String s = new String("ab")
    }
}

会创建几个对象呢?
创建了两个对象 0 和4一个对象是通过new关键字在堆间中创建 的另一个对象是,字符串常量池中ldc创建的具体看字节码指令


0 new #2 <java/lang/String>
3 dup
4 ldc #3 <Test>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return

 String s1 =  new String("Te")+ new String("st");

会创建几个对象呢? 准确来说有6个

  // 创建了5个对象
        /*
        对象一:StringBuilder 只要有拼接操作就会首先创建一个StringBuilder
        对象二:String
        对象三:字符串常量池 Te
        对象四:String
        对象五:字符串常量池 st

        深入剖析:StringBuilder到的toString():
                  对象6:new String("Test")
                  强调一 在字符串常量池中没有生成"Test",下面的字节码指令中没有ldc,没有在字符串常量池中进行创建对象
      字节码指令如下:
             0 new #80 <java/lang/String>
             3 dup
             4 aload_0
             5 getfield #234 <java/lang/StringBuilder.value : [C>
             8 iconst_0
             9 aload_0
            10 getfield #233 <java/lang/StringBuilder.count : I>
            13 invokespecial #291 <java/lang/String.<init> : ([CII)V>
            16 areturn

         */
        String s1 =  new String("Te")+ new String("st");
        /*
        *0 new #2 <java/lang/StringBuilder>
         3 dup
         4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
         7 new #4 <java/lang/String>
        10 dup
        11 ldc #5 <Te>
        13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
        16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
        19 new #4 <java/lang/String>
        22 dup
        23 ldc #8 <st>
        25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
        28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
        31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
        34 astore_1
        35 return
        * */

在一般比较大型的社交网站,很多人都存储字符串,所以使用intern(),就会明显降低内存的大小。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最好的文酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值