1. String 基本特性

String: 代表不可变的字符序列。简称: 不可变性。

  • 当对字符串 重新赋值 时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值。
  • 当对现有的字符串进行 连接操作 时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。
  • 当调用 String 的 replace() 方法修改 指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。
  • String 实现了 Serializable 接口: 表示字符串是支持序列化的。实现了 Comparable 接口: 表示 String 可以比较大小
  • 通过 字面量的方式(区别于 new) 给一个字符串赋值,此时的字符串值 声明在字符串常量池中。

2. 存储结构变更

String 在 jdk8 及以前内部定义了 final char[] value 用于存储字符串数据。jdk9 时改为 final byte[] value

3. StringTable

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230427-1530%%
❕ ^2hufrh

3.1. 特性

image-20200131011131973

image.png

3.2. 存放内容

字符串常量

3.3. 位置

3.3.1. 版本演变

总结:

原来 1.6 时,串池放在永久代,之后 fullGC 时才能垃圾回收,增加了字符串内存占用时间,容易导致永久代内存不足,所以从 1.7 之后,把串池放到堆中,miniGC 就可以对串池进行垃圾回收。

image.png

3.3.2. 用代码验证 stringtable 位置⭐️🔴

%%
▶10.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230311-1306%%
❕ ^ownhou

JDK1.6

image-20200131021306594
image.png

JDK1.8
image.png

image-20200131021234495

处理办法:

3.3.3. 静态变量本身存放位置

JDK9 之后查看工具:性能调优专题-基础-9、JVM-调优工具

静态变量位置的变化:指的是静态变量的引用,而不是静态变量等号后面的对象,等号后面 new 的对象始终都在堆上。

image.png

3.4. 调优⭐️🔴

image-20200131094814790

image-20200131094522452

line 改为 line.intern(),总结:如果项目中存在大量字符串,并且有大量重复值,则可以用入池动作来减少堆内存的消耗

https://www.javatt.com/p/47643

但是如果大量且不重复,就不能使用 intern 功能!否则 YGC 时间会变长

性能调优-进阶-2、系统调优案例♨️

3.5. 参考

https://blog.csdn.net/u011635492/article/details/81046174
https://blog.csdn.net/qq_26222859/article/details/73135660
https://www.bilibili.com/video/av70549061?p=36

[[JVM系列-第9章-StringTable(字符串常量池)]]

4. 拼接操作

https://www.bilibili.com/video/BV1PJ411n7xZ?t=351.8&p=122

1.常量与常量的拼接结果在常量池,原理是编译期优化

2.常量池中不会存在相同内容的常量。

3.只要其中有一个是变量,结果就在堆中,相当于 new String()。变量拼接的原理是 StringBuilder %%
▶16.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230311-1853%%
❕ ^1el21s

4.如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

image-20211006082900880

4.1. 编译期优化

“a”、”b” 都是常量,运行期不会再改变,所以编译期直接优化为 “ab”

4.2. 常量拼接

%%
▶17.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230311-1902%%
❕ ^rgva19

image.png

4.3. append 与 +

JDK1.6 之后,字符串的 + 操作被优化成 StringBuilder 的 append 方法,所以问 String 和 StringBuffer 的区别,在使用的时候,本质是 StringBuilder 和 StringBuffer 的区别,区别就是 StringBuffer 是线程安全的,StringBuilder 是线程不安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StringTest5 {

public static void main(String[] args) {
long timeStart = System.currentTimeMillis();
StringTest5 stringTest5 = new StringTest5();
// stringTest5.methodByString(100000); //用时: 5174
stringTest5.methodByStringBuilder(100000); //用时: 4
long timeEnd = System.currentTimeMillis();
System.out.println("用时:"+ (timeEnd-timeStart));
}
public void methodByString(int times){
String str="";
for (int i = 0; i < times; i++) {
str= str+ "a"; // 每次循环都创建一个StringBuilder
}
}


public void methodByStringBuilder(int times){
StringBuilder str = new StringBuilder(); // new StringBuilder(times);
for (int i = 0; i < times; i++) {
str.append("a");
}
}
}

很显然,对于任意的一个 demo,当执行次数越大,拼接的效率远低于 StringBuild 的 append(),花费的时间比远超两三个数量级

  • 体会执行效率: 通过 StringBuilder 的 append() 的方式添加字符串的效率要远高于使用 string 的字符串拼接方式!
  • ① StringBuilder 的 append() 的方式: 自始至终中只创建过一个 StringBuilder 的对象
    使用 string 的字符串拼接方式: 创建过多个 StringBuilder 和 String 的对象
  • ② 使用 String 的字符串拼接方式: 内存中由于创建了较多的 StringBuilder 和 String 的对象,内存占用更大:

改进:
在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值 highlevel 的情况下建议使用构造器实例化:
stringBuilder s = new StringBuilder(highLevel);//等价于 new char[highLevel]

5. 几个对象⭐️🔴

%%
▶18.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230311-1927%%
❕ ^8yadcm

new String("ab") 是创建了 2 个对象,但返回出去的是哪个对象的地址?返回的是堆中对象的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2); //JDK7/8 : FALSE JDK6: FALSE

/* String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); //JDK7/8 : TRUE JDK6: FALSE
*/
}
}


%%
▶19.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230311-1939%%
❕ ^sc631z

6. intern⭐️🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StringIntern {
public static void main(String[] args) {
/* String s = new String("1");
s.intern(); //调用此方法之前 字符串常量池中已经有1了,所以什么也不做
String s2 = "1";
System.out.println(s == s2); //JDK7/8 : false JDK6: FALSE*/

String s3 = new String("1") + new String("1"); // s3 变量的地址: new String("11")
//执行完上一行代码后,字符串常量池是否存在 11 ,答 没有
// 原因是 s3 地址是 StringBuilder 中的 toString 方法的new String() ,此new String () 比较特殊
// ,只会执行在堆中分配 , 不会执行在字符串常量池中分配
s3.intern(); //在字符串常量池中生成 11
// jdk6:创建一个新的对象“11”,有了新的地址
// jdk7:此时常量池中没有创建11 ,而是创新了一个指向堆空间中new String("11")的地址 。
String s4 = "11"; //s4变量记录的地址:使用的是上一行代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4); //JDK7/8 : TRUE JDK6: FALSE
}
}

总结 String 的 intern() 的使用:

6.1. 放对象 copy

  • jdk1.6 中,将这个字符串对象尝试放入串池。【放 copy 对象】
    • 如果串池中有,直接返回已有的串池中的对象的地址
    • 如果没有,会从堆中 将对象复制一份创建一个新对象放入串池,并返回串池中的对象地址

6.2. 放指针

Jdk1.7 起,将这个字符串对象尝试放入串池。【放指针】

  • 如果串池中有,直接返回已有的串池中的对象的地址
  • 如果没有,则会 拿堆中对象的引用地址复制一份,放入串池,并返回串池中的引用地址

6.3. 总结

6.4. 练习题

1
2
3
4
5
6
7
8
9
10
11
12
//复习demo
public class StringExer2 {
public static void main(String[] args) {
//执行完以后,会在字符串常量池中会生成"ab”
string s1 = new string("ab"); //false
//下述执行完以后,不会在字符串常量池中会生成"ab"
//string s1 = new string("a") + new String("b"); //true
s1.intern();
String s2 = "ab" ;
System.out.println(s1 == s2);//false
}
}

7. 3 个池子

性能调优专题-基础-3、JVM-方法区和3个池子

8. 实战经验

8.1. 空间查看

-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails

8.2. 节省空间

8.3. G1 对 String 去重优化

9. 参考与感谢

性能调优专题-基础-4、JVM-堆和GC理论 性能调优专题-基础-4、JVM-堆和GC理论

https://blog.csdn.net/wei198621/article/details/112389917

https://codeantenna.com/a/mu1CwajTRu#33_StringBuildappend_186