String概述

  • 声明为final,不可被继承
  • 实现了Serializable和Comparable接口
  • jdk8及之前用char数组实现,jdk9开始用byte数组实现(加上编码标记)
  • 代表不可变的字符序列

    • 字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值

      JVM中的String Pool

  • 字符串常量池,也称String Table,在Java堆中

  • 固定大小的Hash Table,不能存储相同内容的字符串,完全相同的字符串字面量,应包含同样的Unicode字符序列,指向同一个String类实例
  • -XX:StringTableSize设设置StringTable的长度
  • 声明一个String变量,先使用equals判断常量池中是否有相等的字符串,如果没有则将这个String变量添加到常量池中,如果有则将返回一个地址给新声明的变量
  • 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
  • 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
    • 直接使用双引号声明出来的string对象会直接存储在常量池中
      • 比如:string info = “atgluigu.com”;
    • 如果不是用双引号声明的string对象,可以使用String提供的intern()方法
  • Java 6及以前,字符串常量池在方法区中,Java7开始,字符串常量池的位置调整到Java堆中(新生区)
  • 为什么分配在堆?

    • 元空间垃圾回收频率低

      拼接操作

  • 常量与常量的拼接结果放在常量池,原理是编译器优化

  • 只要其中一个是变量,结果就在堆中,变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串传对象放入池中,并返回对象地址 ```java String s1 = “java”; //常量池 String s2 = “python”; //常量池 String s3 = “javapython”; //常量池 String s4 = “java” + “python”; //常量池已存在 String s5 = “java” + s2; //堆 String s6 = s1 + “python”; //堆 String s7 = s1 + s2; //堆 String s8 = s6.intern(); //常量池已存在

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

  1. <a name="Bp5RQ"></a>
  2. ## 变量拼接的细节
  3. s1 = "a"; s2 = "b"; 则 s1 + s2底层实现为:
  4. 1. StringBuilder s = new StringBuilder();
  5. 1. s.append("a")
  6. 1. s.append("b")
  7. 1. s.toString() --> 类似于 new String("ab")
  8. - 如果引用的是常量或常量引用,则仍然使用编译期优化,那么不会在堆中创建新的对象,在准备变量的时候不是赋零值,而是直接赋常量值
  9. ```java
  10. final String s1 = "a";
  11. final String s2 = "b";
  12. String s3 = "ab";
  13. String s = s1 + s2;
  14. System.out.println(s == s3); //true
  • 执行效率

    1. public class TestString4 {
    2. public static void main(String[] args) {
    3. long begin1 = System.currentTimeMillis();
    4. String string = "nihao";
    5. for (int i = 0; i < 100000; i++) {
    6. string = string + i;
    7. }
    8. System.out.println(System.currentTimeMillis() - begin1);
    9. System.out.println("=======================");
    10. long begin2 = System.currentTimeMillis();
    11. StringBuilder s = new StringBuilder();
    12. for (int i = 0; i < 100000; i++) {
    13. s.append(i);
    14. }
    15. System.out.println(System.currentTimeMillis() - begin2);
    16. }
    17. }
    1. 22840
    2. =======================
    3. 3
  • 通过StringBuilder代替String,改进:StringBuilder s = new StringBuilder(hihtLevel); 避免动态扩容

    intern()

  • 对于任何两个字符串s和t , s.intern() == t.intern()是true当且仅当s.equals(t)是true

  • 如果不是用双引号声明的string对象,可以使用string提供的intern方法:方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如: string myInfo = new string (“I love atguigu” ) .intern() ;
  • 也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true:
    ( “a” +”b” + “c” ) .intern ( ) == “abc”
  • 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(string Intern Pool) 。

    1. public class TestIntern {
    2. public static void main(String[] args) {
    3. String s1 = "yangtao";
    4. String s2 = new String("yangtao").intern();
    5. String s3 = new StringBuilder("yangtao").toString().intern();
    6. System.out.println(s1 == s2);
    7. System.out.println(s1 == s3);
    8. }
    9. }

    以上s1、s2、s3都是指向字符串常量池中的一个地址,指向同一个字符串 “yangtao”

    new String()

  • String str = new String(“ab”); 创建几个对象?

    • 2个,分别为String对象(堆空间)、字符串常量池中的对象 “ab”

image.png

  • String str = new String(“a”) + new String(“b”); 创建了几个对象
    • 6个,StringBuilder、new String(“a”)、”a”、 new String(“b”)、 “b”、new String(“ab”)

image.png

题目

  1. public class TestIntern2 {
  2. public static void main(String[] args) {
  3. String s = new String("1");
  4. s.intern();
  5. String s2 = "1";
  6. System.out.println(s == s2);
  7. String s3 = new String("1") + new String("1");
  8. s3.intern();
  9. String s4 = "11";
  10. System.out.println(s3 == s4);
  11. }
  12. }

结果:false、true

  • false:行3创建了new String(“1”)、”1” 两个对象,因此调用intern()方法前,字符串常量池中就已经存在”1”了,而s是new String(“1”)的引用、s2是”1”的引用,一个指向对象,一个指向字符串常量池,因此结果为false
  • true:行8中创建的对象有5个,分别为StringBuilder、new String(“1”)、”1”、new String(“1”)、new String(“11”),在调用s3.intern()时不存在”11”这个变量,又因为jdk7开始,字符串常量池存储在堆中,因此为了节省空间,String s4 = “11” 并没有在字符串常量池中创建 “11”这个字符串,而是维护了一个引用,指向堆中 new String(“11”),因此s3 == s4结果为true

    1. public class TestIntern3 {
    2. public static void main(String[] args) {
    3. String s3 = new String("1") + new String("1");
    4. String s4 = "11";
    5. s3.intern();
    6. System.out.println(s3 == s4);
    7. }
    8. }

    结果:false,s3指向堆中的一个对象、s4指向字符串常量池的”11”

    总结

  • jdk6中,会将这个字符串对象尝试放入串池,如果串池中有,则不会放入,返回已有串池中的对象的地址;如果没有,会把对象复制一份,放入串池,返回串池中对象的地址

  • jdk7开始,将字符串对象尝试放入串池,如果串池中有,则不会放入,返回已有的串池中的对象的地址;如果没有,则会把对象的引用地址赋值一份,放入串池,并返回串池中的引用地址

    效率测试

    1. public class TestIntern6 {
    2. static final int MAX_COUNT = 1000 * 10000;
    3. static final String[] arr = new String[MAX_COUNT];
    4. public static void main(String[] args) throws InterruptedException {
    5. Integer[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    6. long start = System.currentTimeMillis();
    7. for (int i = 0; i < MAX_COUNT; i++) {
    8. // arr[i] = new String(String.valueOf(data[i % data.length])).intern();
    9. arr[i] = new String(String.valueOf(data[i % data.length]));
    10. }
    11. long end = System.currentTimeMillis();
    12. System.out.println("花费时间为:" + (end - start));
    13. Thread.sleep(1000000);
    14. }
    15. }
  • 如果需要存储大量重复的字符串,那么调用intern()方法,引用指向字符串常量而不是String对象,而String对象可以后期优化消除,因此,可以节约内存空间,加快字符串操作任务的执行速度。

    StringTable的垃圾回收

  • 运行参数:-XX:+PrintStringTableStatistics -XX:+PrintGCDetails

    G1的String去重

    背景

  • 对许多Java应用(有大的也有小的)做的测试得出以下结果

    • 堆存活数据集合里面String对象占了25%
  • 堆存活数据集合里面重复的string对象有13.5%string对象的平均长度是45
  • 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是string对象。更进一步,这里面差不多一半string对象是重复的,重复的意思是说:string1.equals(string2)==true。堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的 String 对象进行去重,这样就能避免浪费内存。

    实现

  • 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。

  • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
  • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
  • 如果存在,string对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
  • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

    命令

  • UseStringDeduplication (bool):开启string去重,默认是不开启的,需要手动开启。

  • PrintstringDeduplicationstatistics (bool):打印详细的去重统计信息
  • StringDeduplicationAgeThreshold (uintx):达到这个年龄的string对象被认为是去重的候选对象