22.StringTable

  • String: 字符串一般用""来表示
    • String s1 = “hello”; // 字面量定义方式
    • String s2 = new String(“haha”); // 对象方式
  • String声明为final 不能继承
  • String 实现了Serializable Comparable 接口
  • JDK8以前内部定义了final char[] value数组来存储字符串数据,JDK9之后改用byte[]

JDK9为什么改用byte[]来存储

  • String类的当前实现将字符存储在char数组中,每一个字符使用2个字节16位.从许多不同的应用程序收集的数据表明,字符串是堆占用的主要部分,而且大多数字符串都只包含拉丁字符.这些字符只需要一个字节存储空间,因此这些字符串对象内部的char数组有一半的空间将不会被使用
  • 改变字符串内部表示class从utf-16字符数组到字节数组+一共encoding的字段,新的String类将根据字符串内容存储编码为ISO-8859-1(每个字符一个字节)或UTF-16(每个字符两字节).编码标志将表示使用哪种编码
  • String JDK8使用char[]存储 JDK9之后使用byte[]加上编码标记存储 节约了一些空间
  • 一切基于String的数据结构,如StringBuffer StringBuilder底层也做了相应的改变

String的不可变性

  • String: 代表不可变的字符序列 具有不可变性

  • 字符串的每次改变对应的都是内存区的重新赋值,不再使用原有的value,原有value保留到GC回收,以下情况都属于字符串改变,重新分配堆内存存储

    • 字符串重新赋值 String s1 = “haha”; s1 = “hehe”;
    • replace方法 s1.replace(“h”,“c”)
    • 字符串拼接 s1+“hahaha”
  • 通过字面量的方法(不同于new)给字符串赋值,此时字符串声明在字符串常量池中

  • 面试题

    • public class StringExer {
          String str = new String("good");
          char [] ch = {'t','e','s','t'};
      
          public void change(String str, char ch []) {
              // 此时str是一个方法类的局部变量 跟成员变量str没有关系 String自身也是不可变的
              str = "test ok";
              ch[0] = 'b';
          }
      
          public static void main(String[] args) {
              StringExer ex = new StringExer();
              ex.change(ex.str, ex.ch);
              System.out.println(ex.str);
              System.out.println(ex.ch);
          }
      }
      // 输出结果
      // good
      // best
      

字符串常量池

  • 字符串常量池不会存储相同内容的字符串
  • 字符串常量池是一个固定大小的HashTable,JDK6以前默认长度是1009,如果放入常量池的字符串非常多,就会造成Hash冲突严重,从而导致链表很长,直接影响就是调用intern()方法性能会大幅下降
  • -XX:StringTableSize 设置 字符串常量池长度
  • JDK6的StringTable是固定的1009,JDK7中默认值是60013,JDK8中StringTableSize最小可以设置的值为1009

String内存分配

  • java中有8种基本数据类型(byte short int long float double boolean char)和一个比较特殊的引用类型String.这些类型为了使他们在运行时速度更快,更节省内存,都提供了常量池
  • 常量池就类似一个java系统级别提供的缓存.8种基本数据类型常量池都是系统协调的.String类型的常量池比较特殊,他使用的方法有两种
    • 直接使用双引号声明出来的String对象直接存储在常量池中 如String s1 = “hahaha”
    • 如果不是用双引号声明的对象,也可以使用intern方法
  • JDK6以前,字符串常量池存放在永久代
  • JDK7 字符串常量池位置调整到了堆中
    • 所有字符串都保存在堆中,和其他普通对象一样,这样可以让你再进行调优应用的时候仅调整堆大小即可
    • 字符串常量池使用得比较多,但是这个概念使得我们有足够的理由重新考虑在JDK7中使用intern()方法
  • JDK8元空间,字符串常量在堆中
  • image-20200711093546398
  • image-20200711093558709
  • 为什么将StringTable从永久代移动到堆中
    • JDK7中,interned字符串不在永久代生成中分配,而是在堆中分配,与应用程序创建的其他对象一起.使得堆中数据更多,永久代中数据更少,更关注的堆调优.
    • 由于这个变量,大多数应用程序在堆中使用方面只会看到相对较小的差异,但加载许多类或大量使用字符串的较大应用程序就会出现差异
    • 永久代默认大小比较小,永久代的垃圾回收频率低,仅在触发Full GC的时候回收

String的基本操作

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

字符串拼接操作

  • 常量和常量的拼接结果再常量池中,原理是编译期优化
  • 常量池中不会存在相同的内容的变量
  • 只要其中有一个是变量,结果就在堆中.变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串放入池中,并返回次对象地址
  public static void test1() {
        String s1 = "a" + "b" + "c";  // 得到 abc的常量池
        String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回
        /**
         * 最终java编译成.class,再执行.class
         */
        System.out.println(s1 == s2); // true,因为存放在字符串常量池
        System.out.println(s1.equals(s2)); // true
    }

	public static void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";
        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";    
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        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

        String s8 = s6.intern();
        System.out.println(s3 == s8); // true
    }
  • 如果拼接符号前后出现了变量,则相当于在堆空间new String(),具体的内容为拼接的结果

  • 而调用intern方法,则会判断字符串常量池中是否存在JavaEEhadoop值,如果存在则返回常量池的值,否则就在常量池中创建

  • 底层原理 StringBuilder

    • s1+s2细节

      StringBuilder s = new StringBuilder()
      s.append(s1);
      s.append(s2);
      s.toString(); // 类似 new String()
      
  • JDK5之后使用StringBuilder, JDK5之前使用的是StringBuffer

  • 如果左右两边都是变量,就需要new StringBuilder进行拼接,但是如果使用的是final修饰,则是从常量池中获取,所以说拼接符号左右两边都是字符串或者常量引用则仍然使用编译器优化.也就是被final修饰的变量,将会变成常量,类和方法将不能被继承

  • 开发中能用final尽量都加上final修饰

    特性是否可变线程安全字符串操作
    StringString不可变,导致每次堆String操作会生成一个新的String对象,效率低,浪费大量内存空间YN多线程操作字符串
    StringBuffer可变类,线程安全的字符串操作,任何对它指向的字符串操作都不会产生新对象.每个StringBuffer都有一定缓冲容量,当字符串大小没有超过容量不会分配新的容量,当字符串大于容量时,会自动扩容YN单线程操作字符串
    StringBuilder可变类,线程不安全,速度更快YY

拼接操作和append性能对比

    public static void method1(int highLevel) {
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src += "a"; // 每次循环都会创建一个StringBuilder对象
        }
    }

    public static void method2(int highLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            sb.append("a");
        }
    }

方法1耗费的时间:4005ms,方法2消耗时间:7ms

  • 通过StringBuilder的append方法添加字符串效率远远高于直接拼接
  • StringBuilder append方法始终只有一个StringBuilder对象
  • 对于字符串拼接的方法,还需要创建很多的StringBuilder对象和调用toString方法创建String对象
  • 内存中由于创建了较多的StringBuilder对象和String对象,内存占用过大,如果进行GC将会耗费更多的时间
  • 优化方案
    • StringBuilder默认空参构造器,默认字符串容量是16,然后将原来的字符串拷贝到新的字符串中,也可以初始化的时候使用更大的长度,减少扩容的次数
    • 实际开发中,如果确定添加的字符串不高于某一个限定值,则建议使用构造器创建一个阈值长度

intern方法使用

  • intern是native方法,调用底层C的方法

  • 字符串常量池最初是空的,右String类私有维护.在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串.否则,该字符串对象添加到常量池,并返回对该字符串的对象引用

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

  • String info = new string("hahaha").intern();
    
  • 如果在任意字符串上调用intern方法,那么返回那个类的实例,和直接以常量形式出现的字符串实例完全相同

  • "a"+"b"+"c".intern()=="abc"
    
  • interned String就是确保字符串在内存里只有一份,这样可以节约内存空间,加快字符串操作任务的速度.这个值会被存放在字符串内部池中 String Intern Pool

intern空间效率测试

public class StringInternTest2 {
    static final int MAX_COUNT = 1000 * 10000;
    static final String[] arr = new String[MAX_COUNT];

    public static void main(String[] args) {
        Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX_COUNT; i++) {

            /**
             * 没有使用 intern JProfiler查看 String的实例对象有1000W个.内存占用在240M
             * 使用 intern之后 JProfiler查看 String实例对象180W个,内存占用 42M
             */
//            arr[i] = new String(String.valueOf(data[i%data.length]));
            arr[i] = new String(String.valueOf(data[i%data.length])).intern();
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));

        try {
            Thread.sleep(1000000);
        } catch (Exception e) {
            e.getStackTrace();
        }
    }
}
  • 对于程序中大量使用存在的字符串时,尤其在很多重复字符串时,使用intern方法能节省内存空间
  • 大型网站,内存中需要存储大量的字符串,例如社交网站,很多人都要存储市区信息,这时候如果字符串调用intern方法,就会明显降低内存的大小

面试题一

new String(“ab”) 会创建几个对象?

2个对象,查看字节码发现

"ab"为一个对象存在字符串常量池中 (JDK6是在永久代,JDK7以后在堆空间中单独的区域)

new String中 new出来的对象存在堆空间中

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}
 0 new #2 <java/lang/String> #第一个对象 new String的引用
 3 dup
 4 ldc #3 <ab> # "ab" 为字符串常量池中的对象
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return

面试题二

  • new String(“a”) + new String(“b”) 会创建几个对象
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}

0 new #2 <java/lang/StringBuilder> # 1.存在带有变量的字符串拼接操作第一个new StringBuilder对象
3 dup
4 invokespecial #3 <java/lang/StringBuilder.> # StringBuilder 初始化
7 new #4 <java/lang/String> # 2.new String(“a”)对象
10 dup
11 ldc #5 # 3. "a"字面量在字符串常量池中的对象
13 invokespecial #6 <java/lang/String.> # 初始化new String(“a”)
16 invokevirtual #7 <java/lang/StringBuilder.append> # 执行StringBuilder#append方法将a添加到StringBuidler中
19 new #4 <java/lang/String> # 4. new String(“b”) 对象
22 dup
23 ldc #8 # 5. "b"字面量在字符串常量池的对象
25 invokespecial #6 <java/lang/String.>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString> # 6. 本质上StringBuidler#toString方法也会重新new String 严格将这里还有一个对象
34 astore_1
35 return

  • ​ 通过上面字节码分析一共有6个对象
    1. 存在带有变量的字符串拼接操作第一个new StringBuilder对象
    2. new String(“a”)对象
    3. "a"字面量在字符串常量池中的对象
    4. new String(“b”) 对象
    5. "b"字面量在字符串常量池的对象
    6. 本质上StringBuidler#toString方法也会重新new String(“ab”) 严格将这里还有一个对象

intern方法深度分析

public class StringInternTest3 {

    public static void main(String[] args) {
        /**
         * "1"字面量 已经在常量中添加了一个真实的"1"
         * new String("1") 相当于堆中常量池之外 开辟了空间来存放 与常量池中的"1"地址不同
         */
        String s = new String("1");
        /**
         * 将对象放入常量池  此时常量池中 已经有了"1" 返回常量池的引用,但是这里并没有接受 所以 s变量还是指向非常量池堆空间的地址
         */
        s.intern(); // 将该对象放入到常量池。但是调用此方法没有太多的区别,因为已经存在了1
        String s2 = "1";
        System.out.println(s == s2); // JDK6 false JDK8 false

        /**
         * 1. "1"字符串生成存放在常量池中
         * 2. 字符串拼接操作 底层 StringBuilder
         * 3. append之后最后调用StringBuilder底层使用toString() 返回一个新的String对象在 非常量池堆空间地址
         */
        String s3 = new String("1") + new String("1");
        /**
         * 注意!!!! 此时在JDK6 和JDK7以后出现区别
         * JDK6
         * intern方法 是真正生成一个 "11"字面量存在常量池中 此时s3还是非常量池堆的地址
         * JDK7之后
         * intern方法 为了节省空间 不将真实"11"字面量存在常量池
         * 而是!!! 将s3这个变量的引用存在常量池,从而代替真实"11"
         * 所以此时 字符串常量池中表示"11"的其实存放的是一个非常量池堆空间的地址!! 所以jdk7之后比较为true
         */
        s3.intern();
        /**
         * 此时字符串常量池中已经有了"11"这个字面量
         * JDK6 和JDK7以后的区别在于
         * JDK6 是真实"11"在常量池中的地址 而s3还是非常量池堆的地址,故比较为false
         * JDK7之后 为了节约空间,不在重新生成一个真实的"11",常量池中表示"11"常量的实际上是存的s3的地址引用,
         * 返回给s4其实就是s3的地址,故比较为true
         */
        String s4 = "11";
        System.out.println(s3 == s4); // JDK6 false JDK8 true
    }
}

image-20200711145925091

拓展

    public static void extra() {
        String s = new String("1");
        s = s.intern();
        String s2 = "1";
        System.out.println(s == s2);//true
    }

intern将返回值重新赋值给s之后 此时s指向就是字符串常量池中的"1"

拓展二

    public static void extra2() {
        String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4); //false
    }
  • 此时先生成了"11"字面量放在常量池中 此时常量池保存的是真实的"11" 跟之前的区别在于s4的定义在s3.inter()方法之前
  • 在调用s3.intern()方法之前. “11"已经因为String s4=“11” 这句话将真实的"11"存在常量池中,此时常量池中存储的真实的"11” 而不是在s3的地址
  • 此时s3==s4为false

拓展三

public static void extra3() {
    String s1 = new String("1") + new String("1");
    s1.intern();
    String s2 = "11";

    String s3 = new String("1") + new String("1");
    String s4 = "11";

    System.out.println(s1 == s2); // true 原理跟之前一样 常量池中"11"其实是存储的s1的引用
    System.out.println(s1 == s3); //false s3是新的一个String 存在非常量堆空间中
    System.out.println(s1 == s4);//true 原理跟s1==s2一样
    System.out.println(s3 == s4);//false 看似相同的操作, 因为此时s4的地址其实是常量池中s1的地址,但s3是重新new出来的是在堆中一个新地址

}

拓展四

public static void extra4() {
    String s1 = new String("1") + new String("1");
    String s2 = s1.intern();
    System.out.println(s2 == "11"); //JDK6 true  JDK8 true
    System.out.println(s1 == "11"); //JDK6 false JDK8 true
}

拓展五

    public static void extra5() {
        String s1 = "11";
        String s2 = new String("1") + new String("1");
        String s3 = s2.intern();
        System.out.println(s1 == s3);//JDK6 JDK8 相同 true
        System.out.println(s1 == s2);//JDK6 JDK8 相同 false
    }

intern的总结

  • JDK6
    • 如果常量池有,则不会放入,返回已有常量池中的对象地址
    • 如果没有,会把对象复制一份,放入常量池,返回常量池的对象的地址
  • JDK8
    • 如果常量池有,则不会放入,返回已有常量池中的对象地址
    • 如果没有,将对象的引用地址复制一份,放入常量池,返回的是引用地址

StringTable 的垃圾回收

  • 核心JVM参数 -XX:+PrintStringTableStatistics 打印StringTable的统计信息

  • StringTable statistics:
    Number of buckets : 60013 = 480104 bytes, avg 8.000
    Number of entries : 59012 = 1416288 bytes, avg 24.000
    Number of literals : 59012 = 3363568 bytes, avg 56.998
    Total footprint : = 5259960 bytes
    Average bucket size : 0.983
    Variance of bucket size : 0.780
    Std. dev. of bucket size: 0.883
    Maximum bucket size : 5

G1中的String去重操作

  • 这里说的去重指在堆中的数据,而不是常量池中的数据,常量池中的数据本身就不会重复

描述

背景:对于许多java应用测试如下

  • 堆存活数据集合里面String对象占了25%
  • 堆存活数据集合里面重复的数据占13.5%
  • String对象的平均长度45

许多大规模java应用程序瓶颈在于内存,测试表明,在这些类型的应用里面,java堆中存活的数据集合差不多25%是String对象,更进一步,里面差不多一半的String对象都是重复的,重复就是说String#equals方法返回true.堆上存在重复的String对象必然是一种内存浪费.这个项目将在G1垃圾收集器中实现自动持续堆重复String对象去重,避免浪费内存

去重实现原理

  • 当GC工作时候,会访问堆上存活的对象,对于每一个访问的对象都会检查是否候选的要去重string对象
  • 如果是,把这个对象的一个引用插入到队列中等到后续的处理.一个去重的线程后台运行,处理这个队列.处理队列的一个元素以为着从队列删除这个元素,然后尝试去去重他引用的string对象
  • 使用一个hashtable来记录所有的被string对象使用的不重复char数组.当去重的时候,查询hashtable,来看对上是否已经存在一个一模一样的char数组
  • 如果存在.string对象会被调整引用到那个数组,释放堆原来的数组的引用,最终被GC回收
  • 如果查询失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了

JVM参数

  • -XX:UseStringDeduplication=true: 开启String去重,默认不开启
  • -XX:+PrintStringDeduplicationStatistics
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值