String类的基本特性
- 不可变的, 不可被继承的(final类), 支持序列化(实现了 Serializble接口), 可以比较大小(实现了 Comparable接口)
两种定义方式
- 字面量方式: 也就是直接使用双引号括起字符串的声明方式
- 此方式会将 String对象直接存储在字符串常量池中
String s1 = "abc";
- new关键字定义方式:
String s2 = new String("abc");
# 以上方式也可以使用 intern()方法, 将值存到字符串常量池中
s2.intern();
设置 StringTable
- Jdk6中 StringTable默认的 bucket数量是1009, 但 Jdk7开始该默认值被改为60013. 还有 Jdk8开始可以设的最小值为1009. 调优参数:
-XX:StringTableSize=N
- 字符串常量池(String Pool)内部实现是固定大小的 Hashtable(数组加链表的结构), 也就是当字符串常量池大小设小了, 就会造成很多 Hash冲突, 从而导致产生很多链表, 且链表会很长, 由此会直接影响字符串常量池的性能
-XX:+PrintStringTableStatistics
打印 StringTable的统计信息(默认未启用)- 性能演示代码:
public class App {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String s1 = "全大爷";
for(int i = 0;i < 99999;i++) {
String s2 = s1 + i;
s2.intern();
}
long endTime = System.currentTimeMillis();
System.out.println("所花费的时间: " + (endTime - startTime) + "毫秒");
}
}
# 当参数: -XX:StringTableSize=1009
> 所花费的时间: 729毫秒
# 当参数: -XX:StringTableSize=10090
> 所花费的时间: 187毫秒
JVM版本的演变
- Jdk5之前拼接字符串(字节码指令成面上的自动拼接)是通过 StringBuffer做的, 之后改为 StringBuilder
- Jdk6或之前的版本是字符串常量池存放在永久代中. 但因为方法区的垃圾回收效率低下等原因, 之后的版本中转移到了堆空间内
- Jdk6中 StringTable的默认大小是1009, 但 Jdk7开始该默认大小被改为60013. 还有 Jdk8开始可以设的最小值为1009
- 堆上存储的数据中占大部分的是字符串. 且大部分字符串中包含 ISO-8859-1/Latin-1等单字节编码. 在 Jdk8及以前的版本中将这些编码文字都统一存到 String类内部的 final char value[]中, 但由于 char是固定占2个字节的, 因此在存储数据时每个字都固定使用2个字节, 导致浪费了很多空间. 所以 Jdk9开始将 char改为 byte, 将每个字都按字节存储, 以此节约了大量的内存空间来改善了 GC性能
字符串拼接操作
- 常量与常量的拼接结果在常量池, 原理是编译期优化(通过字节码指令可以确认)
- 常量池中不会存在相同内容的常量
- 只要其中有一个是变量, 结果就会存到对象实例中, 变量拼接是通过 StringBuilder实现的
- 如果拼接的结果调用 intern()方法, 则主动将常量池中还没有的字符串对象放入池中, 并返回此对象地址
public class App {
public static void main(String[] args) {
# 在编译期时自动优化: 等同于 "abc"
String s1 = "a" + "b" + "c";
# 以下的定义中 "abc"是从常量池中获取的地址, 然后赋给 s2
String s2 = "abc";
# 比较地址
System.out.println(s1 == s2); // true
# 比较内容
System.out.println(s1.equals(s2)); // true
String s3 = "c";
# 以下的定义里包含一个变量, 所以结果是 StringBuilder对象实例
String s4 = "ab" + s3;
# s2("abc"常量池)的地址与 s4(`StringBuilder对象实例的toString(), 约等于 new String("abc")`)的地址比较, 所以为 false
System.out.println(s2 == s4); // false
# 比较内容
System.out.println(s2.equals(s4)); // true
# s4按当前内容获取了常量池的地址所以相等
System.out.println(s2 == s4.intern()); // true
final String s5 = "c";
final String s6 = "ab";
# 由于 s6和 s5都是常量, 所以结果是常量引用
String s7 = s6 + s5;
System.out.println(s2 == s7); // true
}
}
intern()的使用
- new String(“ab”); 会创建几个对象?
- 一个是在堆上创建的实例
- 再一个是常量池里加一个"ab"
如果之前已经在常量池里创建过"ab", 就是1个, 否则2个
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String ab
- String str = new String(“a”) + new String(“b”); 会创建几个对象?
- 0: new StringBuilder 创建 StringBuilder的实例
- 7: new String 创建 String的实例
- 11: ldc a 创建"a"的常量, 同时 StringBuilder.append(“a”)
- 19: new String 创建 String的实例
- 23: ldc b 创建"b"的常量, 同时 StringBuilder.append(“b”)
- 再深入就会 StringBuilder.toString(); toString的内部实现是 new String()
* new String("a") + new String("b")的计算中最后会产生 new String("ab"); 此时按版本有不同的处理:
- Jdk6时会将"ab"存到常量池; 也就是到以上步骤是, 共创建7个对象
- Jdk7/8时不会将"ab"存到常量池; 也就是到以上步骤是, 共创建6个对象
# javap -v App.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String a
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String b
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
- 例子01
public class App {
public static void main(String[] args) {
String s1 = new String("1");
s1.intern(); // 此时没什么作用, 因为常量池里已存在常量"1"
String s2 = "1";
System.out.println(s1 == s2); // 输出: false, 因为 s1是通过 new创建的实例地址
}
}
# javap -v App.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String 1
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String;
14: pop
15: ldc #3 // String 1
17: astore_2
18: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_1
22: aload_2
23: if_acmpne 30
26: iconst_1
27: goto 31
30: iconst_0
31: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
34: return
- 例子02
* 以下代码中产生(常量"11")的代码行; 按版本的处理是:
- Jdk6时在 new String("1") + new String("1")的计算中创建了常量"11"
- Jdk7/8时在 new String("1") + new String("1")的计算中创建了 new String("11")实例, 而不会创建常量"11"
- 因此 Jdk6时 s3.intern()没什么作用, 但 Jdk7/8时 s3.intern()会创建常量"11"(不过此常量比较特殊, 这个常量与堆上的 new String("11")实例属同一个地址)
- 最后 s3和 s4的比较:
(1) 版本 Jdk6中 s3是 new String("11")的实例, 而 s4是常量"11", 所以不相等 false
(2) 版本 Jdk7/8中 s3是 new String("11")的实例, 而 s4是与 new String("11")的实例地址相等的常量"11", 所以相等 true
public class App {
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); // Jdk6 输出: false, Jdk7/8 输出: true
}
}
# javap -v App.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String 1
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #5 // String 1
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: aload_1
36: invokevirtual #9 // Method java/lang/String.intern:()Ljava/lang/String;
39: pop
40: ldc #10 // String 11
42: astore_2
43: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_1
47: aload_2
48: if_acmpne 55
51: iconst_1
52: goto 56
55: iconst_0
56: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
59: return
-
如果换了 String s4 = “11” 和 s3.intern()的位置, 在版本 Jdk7/8中比较结果将是 false. 因为正常的常量"11"预先生成了, 而不是指向堆实例的常量
-
如果再 String s5 = s3.intern()将拿 intern的结果比较判断, 又会是 true. 就是将字符串实例内容存储到常量池的过程
-
例子03
public class App {
public static void main(String[] args) {
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
# Jdk6
System.out.println(s1 == "ab"); // true
System.out.println(s2 == "ab"); // false
# Jdk7/8
System.out.println(s1 == "ab"); // true
System.out.println(s2 == "ab"); // true
}
}
String的垃圾回收
- G1(Jdk8默认的垃圾回收器)中 String去重操作指的是 String类内 private final char value[]的去重复, 不是指常量池里的(常量池本身就是不重复)
相关调优参数
-XX:+UseStringDeduplication
开启 String去重(默认未启用)-XX:+PrintStringDeduplicationStatistics
打印去重统计信息-XX:StringDeduplicationAgeThreshold=3
字符串对象经过3次 GC后, 如果还存活着, 就会成为去重的候选对象(默认3次)
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!