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

String s1 = "psj";
String s2 = s1.replace('j', 'w')
System.out.println(s1);  // psj
System.out.println(s2);  // psw
  • 字符串常量池不会存储相同内容的字符串(因为字符串常量池是一个固定大小的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));
}
  • 拼接时只要有一个是变量,结果就保存在非常量池部分的堆空间中
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
}

变量拼接原理是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"),这个对象在非字符串常量池部分的堆空间中

​ 注意:只有变量的拼接才是使用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相当于常量)
}

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

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

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


4.intern()的使用

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

在这里插入图片描述

  • 确保字符串在内存中只有一份拷贝,可以节省内存,加快字符串操作的执行速度
  • 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

在这里插入图片描述

在这里插入图片描述

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

注意:比如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(第一个字符进行了修改)
    }
}
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");

在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");

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

在这里插入图片描述


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值