StringTable

1、String的基本特性

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
  • String字符串,使用一对 “” 引起来表示
  • 声明为final,不可被继承
  • 实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口,表示String可以比较大小.
  • String在jdk8及以前内部定义了 final char[] value用于存储字符串数据,jdk9 时改为 byte[]加上编码标记,节约了一些空间
    • 这样调整的原因是一个char是两个byte,而开发中使用数字或英语字符较多,一个byte就能存下,使用char浪费了一半的byte
    • 对于中文等存储时根据字符编码使用两个byte来存储 基于String的StringBuffer,StringBuilder等都做成了更新
  • String :代表不可变的字符序列。简称:不可变性
    • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

1.1 字符串常量池

字符串常量池是不会存储相同内容的字符串的,因为字符串常量池底层是一个固定大小的HashTable(数组+链表),如果放进字符串常量池的String非常多,就会造成Hash冲突,从而导致链表过长,而链表太长的后果就是当调用。

String.intern()(如果字符串常量池中没有String,则在字符串常量池中生成)时性能会大幅下降

  • 可以使用 -XX:StringTableSize设置字符串常量池HashTable的长度
  • JDK6中字符串常量池中的HashTabel的长度默认是1009,所以如果有大量的字符串存入字符串常量池就会导致效率下降
  • JDK7中,字符串常量池中的HashTable默认为60013
  • JDK8及以后,默认为600131009是可以设置的最小值

1.2 new String和直接赋值

除了使用String str = “abc”;外还可以使用String str = new String(“abc”)的形式创建字符串,这两者的机制是不同的

String str = "xxx";
采用字面量直接赋值的方式,Str存储在字符串常量池
    
String str = new String("XXX");
new String对象会发生两种情况:
1.查看字符串常量池中是否有无字符串“xxx”
2.如果有,在堆中Eden区创建”XXX“的拷贝对象
3.如果没有,先将字符串”xxx“放入字符串常量池中
再在堆中Eden区创建一个字符串常量池中”XXX“的拷贝对象

2、String的内存分配

在Java中有8大基本类型,和特殊的String类型,这些类型为了让它们运行过程中更快,更节省内存,都提供了一种常量池的概念,常量池就是一个类似于Java系统级别提供的缓存,8大基本类型的常量池都是系统调节的,而String类型的常量池比较特殊,它的使用方式有两种:双引号声明和new String()

  • 在JDK6时字符串常量池在永久代,
  • 7及以后,字符串常量池被存放在堆中,因为字符串也是对象,需要被回收,而永久代容量较小,存放大量的字符串会OOM,且永久代的GC频率较低,对无效字符串的回收效率很低,就更容易导致永久代OOM.

由new String(“xx”)产生的字符串对象可以看作普通对象,创建后先存储在堆中的Eden区域

3、String的基本操作

3.1 字符串常量池中存储的字符串唯一

如以下程序,在运行至第一个System.out.println();时,字符串常量池中已经存在了2518个字符串,运行至第二个System.out.println();,字符串常量池中有2528个(换行+10个字符串),以后则不再增加。

public static void main(String[] args) {
    System.out.println(); //已经有2518个String了
    System.out.println("1"); //String的数量加1
    System.out.println("2"); // 2520
    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");


    System.out.println(); //2529
    System.out.println("1"); //字符串的数量不变
    System.out.println("2");
    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");
}

在这里插入图片描述

Java规范要求相同的字符串字面量,应该包含相同的Unicode字符序列,且指向同一个String类实例

3.2 字符串常量池存储在堆中

执行toString()时,将生成的字符串以字面量的形式存储到了堆中的字符串常量池,将该字符串的引用保存到了局部变量表

在这里插入图片描述

Heap内存如下:

在这里插入图片描述

4、字符串拼接操作

  1. 常量和常量的拼接结果在常量池,原理是编译期优化
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是常量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
public class StringTest2 {
    public static void main(String[] args) {
        StringTest2 st = new StringTest2();
        st.test();
    }

    public void  test() {
        String s1 = "Java";
        String s2 = "MySQL";

        String s3 = "Java" + "MySQL"; //编译器优化,相当于"JavaMySQL"
        String s4 = "JavaMySQL";

        // 如果字符串拼接的前后过程中出现了变量,则相当于在堆空间中new了一个String()对象
        // 具体的内容为拼接的结果 "JavaMySQL"
        String s5 = s1 + "MySQL";
        String s6 = "Java" + s2;
        String s7 = s1 + s2;
        /*
        String s7 = s1 + s2;的执行细节
        1. StringBuilder s = new StringBuilder();
        2. s.append("Java");
        3. s.append("MySQL");
        4. s.toString()   -> 约等于 new String()

        在JDK1.5 之前使用的是StringBuffer,在1.5之后使用StringBuilder
         */


        System.out.println(s3 == s4); //true
        System.out.println(s3 == s5); //false
        System.out.println(s3 == s6); //false
        System.out.println(s3 == s7); //false
        System.out.println(s5 == s6); //false
        System.out.println(s5 == s7); //false
        System.out.println(s6 == s7); //false

        //intern()判断字符串常量池中是否存在"JavaMySQL"值,如果存在,返回常量池中"JavaMySQL"的地址值
        //如果不存在,在字符串常量池中加载一份"JavaMySQL",将其地址返回
        String s8 = s6.intern();
        System.out.println(s3 == s8); //true
    }
}

4.1 带变量的字符串拼接的底层原理

以上面的代码为例:

String s1 = "a";
String s2 = "b";
String s3 = "c";

String s4 = s1 + "b";
String s5 = s2 + s3;
 0 ldc #6 <a>
 2 astore_1
 //从常量池中取出a, 存放在局部变量表的slot1处
 
 3 ldc #7 <b>
 5 astore_2
 //从常量池中取出b, 存放在局部变量表的slot2处
  
 6 ldc #8 <c>
 8 astore_3
 //从常量池中取出c, 存放在局部变量表的slot3处
 
 /** String s4 = s1 + "b"; */
 9 new #9 <java/lang/StringBuilder>
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
new一个StringBuilder对象,执行其构造方法

16 aload_1
17 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot1取出值a,调用StringBuilder.append(a)

20 ldc #7 <b>
22 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从常量池中取出值b,调用StringBuilder.append(b)

25 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
28 astore 4
// 调用toString()方法,生成一个新的String对象,存放到局部变量表slot4处

 /** String s5 = s2 + s3; */
30 new #9 <java/lang/StringBuilder>
33 dup
34 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
new一个StringBuilder对象,执行其构造方法

37 aload_2
38 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot2取出值b,调用StringBuilder.append(b)

41 aload_3
42 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//从局部变量表的slot3取出值c,调用StringBuilder.append(c)

45 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
48 astore 5
// 调用toString()方法,生成一个新的String对象,存放到局部变量表slot5处
50 return

4.2 直接拼接于append的效率比较

public class AppendEfficiency {
    public static void main(String[] args) {
        AppendEfficiency ae = new AppendEfficiency();
        ae.testString(); //8931
        ae.testStringBuilder(); //10
        ae.testStringBuffer(); //16
    }

    // 测试String的拼接效率
    public void testString() {
        long start = System.currentTimeMillis();

        String s = "";
        for (int i = 0; i < 200000; i++) {
            s += "a";
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    // 测试StringBuilder的拼接效率
    public void testStringBuilder() {
        long start = System.currentTimeMillis();

        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 200000; i++) {
            builder.append("a");
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    // 测试StringBuffer的拼接效率
    public void testStringBuffer() {
        long start = System.currentTimeMillis();

        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 200000; i++) {
            buffer.append("a");
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

对应同样拼接200000次字符串,String和StringBulider、StringBuffer完全不在一个量级上,最快的是StringBulider

在带变量的直接拼接底层页式通过StringBulider的append()实现

但是200000次拼接就生成了200000个StringBuilder对象,每个StringBuilder还需要调用toString(),因此又生成了200000个String对象。

生成200000个StringBuilder对象和200000个String对象的过程也需要时间,且浪费了大量堆空间。并且每次循环前的StringBuilder和String对象失去了引用,需要被回收,频繁的GC又会导致多次STW,导致整个用户线程执行速度降低

而使用1个StringBuilder或StringBuffer完成拼接能节约数倍的空间和时间

优化建议

  1. 面对大量的字符串拼接,在循环外创建一个StringBuilder,使用append()添加字符串
  2. StringBuilder初始数字的长度是16,每次容量不够时,就需要进行扩容,每次扩容都是一次拷贝,拷贝也是需要花时间的。如果大概知道需要多少空间,使用StringBuilder的带参构造方法指定容量大小。

5、intern()的使用

对于非字面量声明的String对象,可以使用String类提供的intern()方法,intern()会从字符串常量池中查询当前字符串是否存在,若不存在则将当前字符串放入字符串常量池,并返回其在字符串常量池的地址,如果存在,则直接返回字符串常量池中对应字符串的地址

String str = new String("lcp").intern();

intern()方法确保字符串常量池中存在一份拷贝
这样可以节约内存空间,加快字符串操作的执行速度

在JDK6及之前 intern()方法先检查字符串常量池有无对应字符串对象
无则在字符串常量池中创建新对象,并返回新对象的地址
有则返回字符串常量池中已存在对象的地址

在JDK7及之后 intern()方法先检查字符串常量池有无对应字符串对象
无则将该对象的地址保存在字符串常量池,并返回该地址
有则返回字符串常量池中已存在对象的地址
public static void main(String[] args) {
    //先通过new String 在字符串常量池和Eden区中分别存放一份“lcp”
    //调用intern方法,在字符串常量池中查找是否存在“lcp”,如果找到,返回字符串常量池中“lcp”的地址
    //str就指向了字符串常量池中“lcp”
    String str = new String("lcp").intern();

    // 以字面量形式声明的字符串放在字符串常量池中,且已存在
    // str也指向字符串常量池中“lcp”, 两者相同
    System.out.println(str == "lcp"); //true
}

5.1 关于intern的练习

练习1:

String s1 = new String("1");  //这个是new的对象的地址
s1.intern();
String s2 = "1";  //这个是字符串常量池中的地址
System.out.println(s1 == s2); // false

在这里插入图片描述

练习2:

String s3 = new String("1") + new String("1"); //s3记录的地址为new String("11")的地址,字符串中不存在"11"
s3.intern();   //在字符串常量池中生成"11",jdk7/8后,因为堆中已经存在了"11",那么字符串常量池中实际保留的是s3的地址
String s4 = "11"; //指向了字符串常量池中的"11"的地址
System.out.println(s3 == s4); // true

在这里插入图片描述

6、G1对堆中String的去重

对于很多Java应用做出测试得到一个结果:

  1. 堆中存活的数据String占了25%
  2. 堆中存活的重复的String对象有13.5%
  3. String对象的平均长度是45B

许多大规模Java应用的瓶颈在于内存,测试表明这些类型的应用中,堆中存在大量的重复字符串对象,即str1.equals(str2) = true,堆中重复的String对象是一种内存的浪费,为此G1垃圾收集器提供了对堆中重复的String对象去重的功能,以减少内存浪费,提高程序性能.

在这里插入图片描述

HotSpot VM对堆中重复String去重的操作默认是关闭的,需要手动打开:

-XX:+UseStringDeduplication,只适合G1

7、补充

7.1 测试new一个String创建了几个对象?

public class StringNewTest {
    public static void main(String[] args) {
        /** new String("ab") 创建了几个对象?   2个(1个String对象 + 1个字符串常量池对象)*/
        /**
         * 0 new #2 <java/lang/String>  创建一个String对象,存在于堆中
         * 3 dup
         * 4 ldc #3 <ab>  在字符串常量池中也有一个ab对象
         * 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
         * 9 astore_1
         */
        String s1 = new String("ab");



        
    }
}

7.2 new String(“a”) + new String(“b”) 创建了几个对象?

public static void main(String[] args) {
    /** new String("a") + new String("b") 创建了几个对象?
         * 6个(1个StringBuilder + 2个String + 2个字符串常量池 + 1个返回的toString
         * 字符串常量池中不存在 "ab"
         */

    /**
         *  new #5 <java/lang/StringBuilder>  创建一个StringBuilder对象
         * 13 dup
         * 14 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
         * 17 new #2 <java/lang/String>  创建一个String对象
         * 20 dup
         * 21 ldc #7 <a>  常量池中存在一个 "a"对象
         * 23 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
         * 26 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
         * 29 new #2 <java/lang/String> 创建一个String对象
         * 32 dup
         * 33 ldc #9 <b> 常量池中存在一个 "b"对象
         * 35 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
         * 38 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
         * 41 invokevirtual #10 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>  再返回一个对象
         * 44 astore_2
         */
    String s2 = new String("a") + new String("b");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值