• JVM内存和垃圾回收-12.String Table


    1.String的基本特性

    • 声明为final,不可被继承

    • 实现Serializable接口,表示其支持序列化

    • 实现Comparable接口,表示其可比较大小

    • JDK8内部使用final char[] value存储字符串数据;JDK9改为byte[](节约空间,因为大部分String对象都是Latin-1字符,此类字符只需要一个byte存储)

    • 不可变的字符序列,下面三种情况都需要重新指定内存区域赋值,不能使用原有的value进行赋值:

      • 当字符串重新赋值时
      String s1 = "psj";
      String s2 = "psj";
      s2 ="psw";
      System.out.println(s1==s2);  // false
      
      • 1
      • 2
      • 3
      • 4
      • 当对现有的字符串进行拼接时,需要重新指定内存区域赋值,不能使用原有的value进行赋值
      String s1 = "psj";
      String s2 = "psj";
      s2 += "2"
      System.out.println(s1);  // psj
      System.out.println(s2);  // psj2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 使用replace方法修改字符串时
    • 通过字面量给字符串赋值(即String a = "psj")时,字符串值声明在堆中的字符串常量池中

    String s1 = "psj";
    String s2 = s1.replace('j', 'w')
    System.out.println(s1);  // psj
    System.out.println(s2);  // psw
    
    • 1
    • 2
    • 3
    • 4
    • 字符串常量池不会存储相同内容的字符串(因为字符串常量池是一个固定大小的Hashtable)。同时为了减少Hash冲突(当放入字符串常量池的String较多时,导致链表过长,进而String.intern时性能下降),可以加大StringTable的长度

    2.String的内存分配

    • 对于8种基本数据类型和String类型,为了让它们运行速度更快且节省内存,都提供了常量池(类似于Java系统级别的缓存,基本数据类型的常量池由系统协调)
    • JDK6及之前,字符串常量池放在永久代(分配内存小,垃圾回收频率低);JDK7/8放置在堆(分配内存大,垃圾回收频率高)

    3.字符串拼接操作

    • 常量和常量的拼接结果放置在常量池中(编译期优化)
    // 源代码
    public void test1(){
        String s1 = "a" + "b";
        String s2 = "ab";
        System.out.println(s1 == s2);  // true
        System.out.println(s1.equals(s2));  // true
    }
    // 编译后的字节码文件
    public void test1() {
        String s1 = "ab"; 
        String s2 = "ab";
        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 拼接时只要有一个是变量,结果就保存在非常量池部分的堆空间中
    public void test2() {
        String s1 = "javaEE";
        String s2 = "hadoop";
        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";  // 编译期优化
        String s5 = s1 + "hadoop";  // 拼接中出现变量,则相当于在堆中new String()
        String s6 = "javaEE" + s2;  // 拼接中出现变量,则相当于在堆中new String()
        String s7 = s1 + s2;  // 拼接中出现变量,则相当于在堆中new String()
        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():判断字符串常量池中是否存在"javaEEhadoop"
        // 存在则返回常量池中该字符串的地址
        // 不存在则在常量池中加载一份该字符串并返回该对象地址
        String s8 = s6.intern();  
        System.out.println(s3==s8);  // true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    变量拼接原理是StringBuilder(JDK8)

    // 源代码:
    public void test3() {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);  // false
    }
    // 字节码文件:对于String s4 = s1 + s2
    StringBuilder s4 = new StringBuilder();
    s4.append("a");
    s4.append("b");
    s4.toString();  // 相当于new String("ab"),这个对象在非字符串常量池部分的堆空间中
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ​ 注意:只有变量的拼接才是使用StringBuilder,常量或常量引用的拼接依旧使用编译期优化

    public void test4() {
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);  // true(s1和s2相当于常量)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ​ 循环进行拼接操作时,直接创建一个StringBuilder对象进行append操作比先创建一个String s = ""再使用加号进行拼接要快许多:

    ​ 后者需要创建多个StringBuilder进行append操作,并且还要进行new String(...)操作

    ​ 后者创建对象较多,占用空间大,GC时间长


    4.intern()的使用

    • 如果不是使用双引号声明的String对象,可调用intern方法,调用后会从字符串常量池中查询当前字符串是否存在,不存在就将该字符串放入常量池
    // 以下两种方式保证变量s指向的是字符串常量池中的数据
    String s = "psj";
    String s = new String("psj").intern();
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    • 确保字符串在内存中只有一份拷贝,可以节省内存,加快字符串操作的执行速度
    • JDK6和JDK7/8/11对于intern方法使用上的区别:针对常量池中没有目标字符串对象时
      • JDK6会复制一份对象
      • JDK7及之后会复制对象的引用地址(假设堆中已经创建了该对象,则引用堆中的对象)
    String s = new String("1");  // s指向堆空间
    s.intern();  // 调用该方法前,字符串常量池中已经存在"1"
    String s2 = "1";
    System.out.println(s == s2);  // false,因为一个是堆空间中的对象,一个是字符串常量池中的对象
    
    String s3 = new String("1") + new String("1");  // s3是堆中的地址,可以理解为new String("11"),反正最后调用toString()(不针对JDK11)
    // 上述拼接代码并没有在字符串常量池中生成"11"(参考面试题3)
    // *执行该行代码时:
    // JDK6:常量池会创建新对象"11"
    // JDK7之后:常量池不会创建"11",而是创建一个对象指向堆中生成的对象(节省空间)
    s3.intern();
    String s4 = "11";  // s4使用的是上一行代码执行后在常量池生成"11"的地址(该"11"在JDK6中是常量池中的,在JDK7及之后是指向堆中创建的"11"对象的地址,s3始终是指向堆中创建的"11"对象的地址)
    System.out.println(s3 == s4);  // JDK7之前为false,之后为true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    在这里插入图片描述

    String s3 = new String("1") + new String("1");  // 还是相当于在堆中new String("11")
    String s4 = "11";  // 在字符串常量池中会生成对象"11"
    s3.intern();  // 检查到在常量池中已经有"11",不创建"11"
    System.out.println(s3 == s4);  // false
    
    • 1
    • 2
    • 3
    • 4

    注意:比如new String("a") + new String("b"),在JDK7及以上最后会调用toString()方法,该方法在字符串常量池中是没有生成"ab"的


    面试题

    1.

    public class Test{  
        String str = new String("good");
        char[] ch = {'t', 'e', 's', 't'};
        public void change(String str, char[] ch){
            str = "test ok";
            ch[0] = 'b';
        }
        public static void main(String[] args) {
            Test t = new Test();
            t.change(t.str, t.ch);
            System.out.println(t.str);  // good(不会变为test ok)
            System.out.println(t.str);  // best(第一个字符进行了修改)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.字符串拼接操作中的示例

    3.

    String s = new String(“psj”)会创建几个对象?两个

    在这里插入图片描述

    new String(“p”) + new String(“s”)创建几个对象?JDK11中创建了4个,JDK8中创建了5个(考虑最后toString方法就是6个)

    注意:这种方式在字符串常量池中最终是没有"ps"对象的,如果使用new String(“ps”)是会在常量池中有"ps"的

    在这里插入图片描述

    String s = new String("a") + new String("b");  // 拼接的方式不会在常量池中生成"ab"
    String s2 = s.intern();  // JDK6会复制对象,JDK7及以上复制对象的引用
    System.out.println(s == "ab");
    System.out.println(s2 == "ab");
    
    • 1
    • 2
    • 3
    • 4

    在JDK6中是true和false:

    在这里插入图片描述

    在JDK7及以上是true和true:

    在这里插入图片描述

    String x = "ab";
    String s = new String("a") + new String("b");  // 还是在堆中创建"ab"对象
    String s2 = s.intern();  // 因为第一行代码执行后已经在常量池中生成了"ab",所以s2直接执行常量池中已有的对象即可
    System.out.println(s == "ab");
    System.out.println(s2 == "ab");
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在所有JDK版本都是false和true:

    在这里插入图片描述


  • 相关阅读:
    [附源码]计算机毕业设计springboot企业售后服务管理系统
    Android WebRTC 入门教程(二) -- 模拟p2p本地视频传输
    云原生Kubernetes:CRI 容器运行时接口
    Java学习07——原码、反码和补码
    反射(类加载、加载流程、加载的五个阶段、获取类结构信息、反射暴破创建实例、操作属性、操作方法)
    Head First设计模式中的典型设计模式解析与案例分析
    SQL注入(get)
    浅析kubernetes中client-go Informer
    SpringCloud源码学习笔记3——Nacos服务注册源码分析
    【数据结构】时间复杂度和空间复杂度
  • 原文地址:https://blog.csdn.net/qq_41398418/article/details/126299625