JVM_01_内存与垃圾回收_06_StringTable


本文介绍JVM中String以及字符串常量池的相关知识。在运行时数据区已经了解了,字符串常量池和静态变量都是存储在堆中的。

1. 概述

String就是字符串,使用一对双引号引起来表示。有两种实例化方式:

  1. String s = "hello";以字符串形式定义,存储在字符串常量池中。
  2. String s = new String("hello");以对象形式定义,存储在堆中。

String声明是final,不可被继承,实现了Serializable接口、Comparable接口。

在JDK1.8的时候,String底层使用的是char数组;在JDK1.9及之后,String底层使用的是byte数组。因为char用的是两个byte,但是在实际生产环境中,堆空间用的数据主要是String,而且存储的都是一些基本字符,完全可以用其ISO-8859-1/Latin-1编码值来表示,这些编码值可以用一个byte来表示,这样就节省了空间。

但是一个byte只能表示基本字符,对于中文仍然不行,需要两个byte来表示。因此,需要一个encoding-flag来表示,如果是UTF-8,则用的是两个字节。

同样,StringBuffer和StringBuilder也进行了相应修改。

字面量的相关规范可参考官网Chapter 3. Lexical Structure (oracle.com)

1.1 字符串常量池

注意,字符串常量池中是不会存储相同内容的字符串的。

字符串常量池是一个固定大小的HashTable。如果放进String Pool的String非常多,就会造成Hash冲突,从而导致列表会很长,而链表长了之后直接会造成的影响就是当调用String.intern()时性能会大幅下降。

这里的意思就是说,首先是Hash运算,如果字符串特别多,或者数组长度太小,显然会冲突,此时就放在对应的链表中,那么此时检索链表,链表的检索效率是比较低的,因此,如果元素过多,效率会大幅下降。

可使用-XX:StringTableSize设置HashTable大小,即数组的长度。

1.2 面试题

  1. new String(“ab”)会创建几个对象?

    2个对象,new String()会在堆中创建一个对象,”ab”会在常量池中创建对象(字节码指令ldc)。

  2. new String(“a”) + new String(“b”)会创建几个对象?

    6个对象,StringBuilder对象,new String(“a”),”a”,new String(“b”),”b”,

    StringBuilder.toString()创建的对象:new String(“ab”)

    注意,字符串常量池中,不存在”ab”。

2. String的内存分配

在Java语言中有8中基本数据类型和一种比较特殊的类型String,这些类型为了使他们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念(基本数据类型的包装类型)。

String类型对象有两种方式,一种是new对象,一种是字符串。采用字符串可以直接在常量池中放一个该字符串,而采用new则需要在堆中创建该对象,如果想要在常量池中放一个字符串,可调用String对象.intern()方法将该值放入到常量池中。

3. 字符串拼接操作

这里的拼接操作指的是加号运算。字符串拼接有如下规则:

  1. 常量与常量的拼接结果在常量池,原理是编译器优化;
  2. 常量池中不会存在相同内容的常量;
  3. 只要其中有一个是变量(final修饰的除外,是常量),结果就在堆中。变量拼接的原理是StringBuilder;
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
1
2
3
4
5
6
7
8
public void m1() {

String s1 = "a" + "b" + "c";
String s2 = "abc";

System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void m2() {

String s1 = "javaEE";
String s2 = "hadoop";

String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;

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
}
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
26
27
28
29
30
public String m3() {
return "hello";
}

public String m4() {
return "world";
}

public void m5() {

String s1 = "hello";
String s2 = "world";

// 注意,这里采用变量接收了,那么只要在拼接的时候用到s3、s4了,肯定和原始字符串不同
String s3 = this.m3();
String s4 = this.m4();

System.out.println(s1 == s3); // true
System.out.println(s2 == s4); // true
System.out.println(s1 + s2 == s1 + s4); // false
System.out.println(s1 + s2 == s3 + s4); // false
System.out.println("hello" + "world" == s3 + s4); // false

// 注意,下面几种情况
System.out.println("hello" == this.m3()); // true
System.out.println("world" == this.m4()); // true

// 方法返回,即使调用的是方法,也相当于有一个变量,可参考IDEA自己反编译后的代码
System.out.println("hello" + "world" == this.m3() + this.m4()); // false
}

3.1 StringBuilder替换字符串拼接

其实上面变量字符串拼接,底层是采用StringBuilder,然后append,最后toString创建字符串对象,并返回该字符串对象。

回想在写程序的时候,涉及到循环字符串拼接,IDEA总是提示采用StringBuilder。这里就是因为加号字符串拼接,每次都创建一个StringBuilder对象。案例如下所示,显然下面的效率要高于上面的。因此,尽量少采用加号字符串拼接,除了创建对象比较多,而且GC的话,需要清理的垃圾也比较多。

1
2
3
4
5
6
7
8
9
10
11
String src = "";
for (int i = 0; i < highLevel; i++) {
src = src + "a"; // 底层每次都创建一个StringBuilder,且toString()会创建一个String对象
}
// -----------------------------------
String src2 = "";
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
stringBuilder.append("a");
}
src2 = src2 + stringBuilder.toString();

4. intern()的使用

字符串对象.intern()方法,判断字符串常量池中是否存在这个字符串值,如果存在,则返回常量池中该字符串的地址;如果不存在,则在常量池中加载一份该字符串,并返回常量池中该字符串的地址。该方法是本地方法。

注意,无论前面是什么,只要最终是intern()方法,返回的都是该字符串在常量池中的地址,如下所示:

1
2
3
String s = "asdf";
String s = new String("asdf").intern();
String s = new StringBuilder("asdf").toString.intern();

补充,上面调用intern()方法,在JDK7及之后,其实不会再在常量池中创建字符串了,而是在常量池中开辟一块空间,保存的就是堆对象的地址【引用】,返回的就是堆对象的那个地址。

但是new String()一定是在堆中创建一个对象,并且在常量池中也创建一个对象。

5. G1中的String去重操作

注意,字符串常量池中本身就不存在重复的字符串,所以这里的去重指的是堆内存中的对象去重。

对许多Java应用做的测试得出以下结果:

  1. 堆存活的数据集合里面String对象占了25%。
  2. 堆存活的数据集合里面String对象有13.5%。
  3. String对象的平均长度是45。

G1垃圾回收器对String对象进行了去重。默认是不开启的。

6. 备注

参考B站《尚硅谷》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录