复习:深度底层理解Java的String

学习内容来自尚硅谷宋红康老师的JVM教程

https://www.bilibili.com/video/BV1PJ411n7xZ/

String的两种创建方式:

  • String s = “abc”; //字面量初始化
  • String s = new String(“abc”); //对象的形式初始化

String类是final的,不可继承

JDK8以及以前版本String的底层是用char[]来进行存储的

JDK9以及以后底层改变为了用byte[]数据进行存储

修改底层结构的字符:String在程序中很常用,并且字符中大部分是拉丁文,即用一个字节就能进行存储,而cahr类型是两个字节的,因此会造成内存空间的浪费。

char[]就改变成了byte[]和对应的编码类型

StringBuilder、StringBuffer、JVM底层处理String的方式自JDK9都做了相应的修改。

String代表不可变的字符序列。

字符串常量池中不会存储相同的字符串。

StringPool 实际是一个固定大小的Hashtable,不能动态修改,常量过多的话Hash冲突会增加。大小可以在JVM的参数上进行定制。

常量池就类似java系统级别的一个缓存,可以让运行速度更快、更节省内存。

JDK6 的时候 字符串常量池在永久代的常量池中,JDK7和JDK8之后都在堆中。

为什么调整?

  • 永久代默认空间比较小,容易溢出
  • 永久代的垃圾回收的频率比较低(或者不回收),字符串回收效率低,也容易溢出

进阶知识

String s = “a” + “b” + “c”;

String s = “abc”

这两句在编译之后是等价的,常量字符串拼接(包括final修饰的变量)是在编译时期进行优化的。即在生成的字节码文件里面是两个一样的字面量“abc”

如果字符串的拼接符号的前后出现了变量,相当于在堆中new了一个String对象。

  • String s1 = “ab”;
  • String s2 = “cd”;
  • String s3 = s1 + s2;
  • String s4 = “abcd”;
  • System.out.println(s3 == s4); // false
  • String s3 = s1 + s2;这一句代码查看底层的字节码可以解释为
    • String tmp = new String();
    • tmp.append(s1);
    • tmp.append(s2);
    • s3 = tmp.toString();

以上导致了对象引用和常量池引用的地址不一致问题。

        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = s + i;
        }
        
        StringBuilder s1 = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            s1.append(i);
        }

上述代码展示的两种字符串拼接方案,效率千差万别

  • append的方式只有一个StringBuilder对象,而字符串拼接的方式每次拼接都会产生一个新的StringBuilder对象以及String对象
  • 其次 ,第一中方式内存中存在过多stringBuilder和String垃圾,如果进行gc需要花费额外的时间。

进一步提高,StringBuilder可以在初始化的时候指定char[]数组的容量,可以避免扩容过程中带来的性能损耗。即如果逻辑大致确定,可以指定一个capacity的上界,就不会扩容了。

intern函数,返回字符串是字符串常量池中的地址(有就直接返回,没有就“创建”,JDK版本有差别)

new String(“ab”); 这条语句会创造几个对象?

一个对象是new 关键字创建的,另一个对象是字符串常量池中的对象。

new String(“a”) + new String(“b); 呢?从字节码的角度看一共有5个对象

  • new StringBuilder()
  • new String()
  • 字符串常量池中的对象“a”
  • new String()
  • 字符串常量池中的对象”b”

StringBuilder的toString方法中 return new String(value, 0, count);分析对应的字节码,得知并没有在常量池中创建“ab”,

字符串常量池的HashTable中放到是什么?现在的理解存到的就是一个完整的String对象的引用,该对象引用和字面量引用是等价的。而new出来的对象是在堆中的。

面试题:

String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);           //false

String s3 = new String("1") +  new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);       //jdk6:false   jdk7/8:ture

第1部分比较简单,就是s1在对空间中创建了一个字符串一的对象,同时字面量一也在字符串常量池里面创建了一个一的对象,而第2句s1.intern();就相当于什么都没做,第3句的S2通过字面量初始化,指向字符串常量池中的那个对象,所以两个地址是不一样。

第2题就比较难,因为它的输出结果和jdk的版本有关,执行intern语句之前,如果字符串常量池中已经有了,就没什么区别。当字符串不存在常量池中的时候,intern方法在1.6中会在永久代中复制一份String对象,并返回相应的地址。而在JDK7/8中,字符串常量池的位置调整到了堆中,调用intern方法的时候如果常量池中没有,则直接指向堆中已有的那个对象,节省空间。

String s = String.valueOf(123);
System.out.println(s == s.intern());    //jdk8 true
String s3 = new String("1") +  new String("1");
String s4 = "11"; 
s3.intern();
System.out.println(s3 == s4);       //jdk7/8:false
String s1 = new String("a") +  new String("b");
String s2 = s1.intern(); 
System.out.println(s1 == "ab");    //jdk6:false   //jdk7/8:true
 System.out.println(s2 == "ab");   //jdk6:true    //jdk7/8:true

在代码中何时的位置加上intern函数可以在一定程度上节省内存空间。因为字符串常量池中的数据可以复用,重复的String对象就可能被gc回收。

发表评论

电子邮件地址不会被公开。 必填项已用*标注