学习内容来自尚硅谷宋红康老师的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回收。
