字符串常量池

文章目录

  • String & StringTable
  • 一、String
    • 1、String 的 基本特性
    • 2、String 的 不可变性
      • 1)重新赋值
      • 2)拼接操作
      • 3)replace()方法
      • 4)方法参数传递
  • 二、StringTable
    • 1、基本概念
    • 2、内存位置
    • 3、大小设置
    • 4、StringTable不会存在相同的String
    • 5、运行时内存案例
  • 三、String的创建与内存分配
    • 1、字面量 / 双引号
    • 2、new关键字
    • 3、StringBuilder.toString()
    • 4、intern() 方法
    • 5、小结
  • 四、字符串的拼接操作
    • 1、举例说明
      • 【例1】常量+常量
      • 【例2】变量拼接
      • 【例3】final变量拼接
    • 2、拼接小结
    • 3、+拼接 vs append拼接
    • 4、StringBuilder的扩容
    • 5、优化小结
  • 五、new String() 会创建几个对象
    • 1、单独new
    • 2、常量 拼接 new
    • 3、new 拼接 new
  • 六、intern()
    • 1、intern() 的作用
    • 2、案例分析
      • 1)案例1
      • 2)案例2
      • 3)案例3
    • 3、intern() 的效率:空间角度
  • 七、StringTable 的垃圾回收
  • 八、G1 中的 String 去重操作
    • 1、背景
    • 2、实现
    • 3、命令行选项

String & StringTable

一、String

1、String 的 基本特性

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // jdk1.8及之前
    private final char value[];

    public String() {
        this.value = "".value;
    }
}
  • String 实现了 Serializable 接口:表示字符串是支持序列化的。
  • String 实现了 Comparable 接口:表示 String 可以比较大小
  • String 声明为 final 的,不可被继承,一旦被创建就不会改变,对字符串的操作只是产生了一个新的字符串。
  • String 在 jdk8及以前 定义了 final char[] value 用于存储字符串数据,jdk9时 改为 final byte[] value

2、String 的 不可变性

String 声明为 final 的,一旦被创建就不会改变String的每次操作都是生成一个新的对象,不改变原来的对象

1)重新赋值

@Test
public void test1() {
    String s1 = "abc";
    String s2 = s1;

    System.out.println(s1.hashCode()); // 96354
    System.out.println(s2.hashCode()); // 96354

    // 不会改变原来的对象("abc"),只是新生成一个对象("hello"),并指向新对象
    s2 = "hello";

    System.out.println(s1.hashCode()); // 96354
    System.out.println(s2.hashCode()); // 99162322

    System.out.println(s1); // abc
    System.out.println(s2); // hello        
}

2)拼接操作

@Test
public void test2() {
    String s1 = "abc";
    String s2 = s1 + "def";

    System.out.println(s1); // abc
    System.out.println(s2); // abcdef
}

3)replace()方法

@Test
public void test3() {
    String s1 = "abc";
    String s2 = s1.replace('a', 'm');

    System.out.println(s1); // abc
    System.out.println(s2); // mbc
}

4)方法参数传递

public class test4() {
    public static void main(String[] args) {
        String str = "old";
        char[] ch = {'t', 'e', 's', 't'};
        change(str, ch);
        System.out.println(str); // old
        System.out.println(ch);  // best
    }
    
    public static void change(String str, char ch[]) {
        // 拼接和replace同理
        str = "new";
        ch[0] = 'b';
    }
}

二、StringTable

1、基本概念

Java 中有 8种基本数据类型1种特殊的引用数据类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了常量池的概念。(常量池就类似一个 Java 系统级别提供的缓存)

8 种基本数据类型的常量池都是系统协调的,而String类型的常量池比较特殊。

2、内存位置

  • Java 6 及以前,字符串常量池存放在永久代
  • Java 7 中,将字符串常量池的位置从永久代 调整到 堆中
  • Java 8 中,字符串常量池存放在堆中

字符串常量池 StringTable 为什么要调整位置?

  • 永久代的回收效率很低,这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,放到堆中能及时回收。
  • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样在进行调优应用时仅需要调整堆大小就可以了。
  • 加载许多类或大量使用 String.intern() 方法的大型应用程序将看到更明显的差异。

3、大小设置

String 的 String Pool 是一个固定大小的 Hashtable(数组+链表)。如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降。

使用-XX:StringTablesize可设置 StringTable 的长度

  • 在 jdk6 中,StringTable 的长度默认值是 1009,如果常量池中的字符串过多就会导致效率下降很快。StringTablesize 设置没有要求
  • 在 jdk7 中,StringTable 的长度默认值是 60013,StringTablesize 设置没有要求
  • 在 jdk8 中,StringTable 的长度默认值是 60013,StringTablesize 可以设置的最小值是1009

4、StringTable不会存在相同的String

Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。

下面我们通过debug以下案例来分析一下:

public class StringTest {
    public static void main(String[] args) {
        System.out.println();	  // 1544
        System.out.println("1");  // 1545
        System.out.println("2");  // 1545
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");  // 1553
        
        // 以下的字符串不会再次加载
        System.out.println("1");  // 1554
        System.out.println("2");  // 1554
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9"); // 1554
    }
}

下面我们通过Debug查看一下idea的Memory中String的个数

字符串常量池_第1张图片

字符串常量池_第2张图片

字符串常量池_第3张图片

字符串常量池_第4张图片

字符串常量池_第5张图片

字符串常量池_第6张图片

5、运行时内存案例

class Memory {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        Memory mem = new Memory();
        mem.foo(obj);
    }

    private void foo(Object param) {
        String str = param.toString().intern();
        System.out.println(str);	// java.lang.Object@42a57993
    }
}

字符串常量池_第7张图片

三、String的创建与内存分配

1、字面量 / 双引号

直接由双引号""给出声明的字符串,存储在字符串常量池中(并且相同的"xxx"只会有一份)

public static void test() {
    String str = "ab";
}
0 ldc #2 
2 astore_0
3 return
  • 使用双引号""创建字符串时,JVM首先会去常量池中查找是否存在这个字符串对象。
  • 不存在:在常量池中创建这个字符串对象,并返回地址。
  • 存在:不创建任何对象,直接返回常量池中字符串对象的地址。

2、new关键字

new 关键字声明的字符串,先在堆内存中创建一个字符串对象(new),然后在字符串常量池中创建一个字符串常量(ldc)。

public static void test() {
    String s1 = new String("ab");
    String s2 = "ab";
}
 0 new #3 
 3 dup
 4 ldc #2 
 6 invokespecial #4  : (Ljava/lang/String;)V>
 9 astore_0
10 ldc #2 
12 astore_1
13 return
  • 使用new创建字符串时,JVM首先会去常量池中查找是否存在这个字符串对象。
  • 不存在:先在常量池中创建一个字符串对象,然后再在堆内存中创建另一个字符串对象。
  • 存在:直接在堆内存中创建另一个字符串对象。

注意:最后返回的是堆内存中字符串对象的地址,不是常量池中的字符串对象的地址。

public static void test() {
    String s1 = new String("ab");
    String s2 = "ab";
    System.out.println(s1 == s2); // false
}

3、StringBuilder.toString()

可以看到,StringBuildertoString()其实会new一个String对象

public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence {
    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
}

需要注意的是,StringBuilder.toString() 不会在常量池中创建对象,下面写个例子分析一下。

public static void test() {
    StringBuilder stringBuilder = new StringBuilder("a");
    stringBuilder.append("b");
    String str = stringBuilder.toString();
}
 0 new #5 
 3 dup
 4 ldc #6 
 6 invokespecial #7  : (Ljava/lang/String;)V>
 9 astore_0
10 aload_0
11 ldc #8 
13 invokevirtual #9 
16 pop
17 aload_0
18 invokevirtual #10 
21 astore_1
22 return

可以看到没有出现 ldc #x ,可见StringBuilder.toString() 只在堆内存创建了一个字符串,并没有放到字符串常量池

4、intern() 方法

intern() 判断字符串常量池中是否存在该字符串,存在,则返回常量池中的地址;不存在,则在常量池中加载一份并返回地址

@Test
public void test() {
    String s1 = "nb";
    String s2 = "n";
    String s3 = s2 + "b";
    String s4 = s3.intern(); // 常量池中存在,返回常量池中的地址

    System.out.println(s1 == s3); // false
    System.out.println(s1 == s4); // true
}

关于intern() 方法下面会详细展开。

5、小结

什么情况下,字符串会被放入字符串常量池呢?

  1. 直接由双引号""给出声明的字符串,会直接放在字符串常量池中。
  2. 使用new创建的字符串,也会有一份放在字符串常量池中。
  3. 调用intern()方法的字符串,也会被放到字符串常量池中。

注意:StringBuilder.toString()生成的字符串,是不会放到字符串常量池中的,只会在堆中创建一份。

四、字符串的拼接操作

1、举例说明

【例1】常量+常量

场景1:常量常量拼接,拼接结果在字符串常量池,原理是编译期优化

public class AppendTest {
    public void test() {
        String s1 = "a" + "b" + "c";
        String s2 = "abc";
        System.out.println(s1 == s2); // true
    }
}

从class文件的反编译结果可以看出:编译器做了优化,将 “a” + “b” + “c” 优化成了 “abc”

 0 ldc #2 
 2 astore_0
 3 ldc #2 
 5 astore_1
 6 getstatic #3 
 9 aload_0
10 aload_1
11 if_acmpne 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #4 
22 return

从IDEA中的AppendTest.class也可以直接看出来

public class AppendTest {
    public AppendTest() {}

    public void test() {
        String s1 = "abc";  // 显示 String s1 = "abc"; 说明做了代码优化
        String s2 = "abc";
        System.out.println(s1 == s2);
    }
}

【例2】变量拼接

场景2:拼接中只要有一个是变量,拼接结果就在堆中,原理是StringBuilderappend操作。

public void test2() {
    String s1 = "n";
    String s2 = "b";

    String s3 = "nb";
    String s4 = "n" + "b"; // 编译期优化
    String s5 = s1 + "b";
    String s6 = "n" + 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

    // 这里使用intern(),会返回常量池中"nb"的地址并赋给s8(这里先了解,具体用法后续会详细展开)
    String s8 = s7.intern();
    System.out.println(s3 == s8); // true
}

下面我们从class文件的反编译结果进行分析

  0 ldc #5 <n>
  2 astore_1
  3 ldc #6 <b>
  5 astore_2
  6 ldc #7 <nb>
  8 astore_3
  9 ldc #7 <nb>
 11 astore 4

s4之前都是【例1】的内容,这里就不赘述了,主要看一下 s5、s6、s7这三行

  • 可以看出,都是先 new 了一个 StringBuilder 对象,然后使用 append() 拼接,最后调用了 toString() 创建 String对象 并赋值
 13 new #8 
 16 dup
 17 invokespecial #9  : ()V>
 20 aload_1
 21 invokevirtual #10 
 24 ldc #6 
 26 invokevirtual #10 
 29 invokevirtual #11 
 32 astore 5
 34 new #8 
 37 dup
 38 invokespecial #9  : ()V>
 41 ldc #5 
 43 invokevirtual #10 
 46 aload_2
 47 invokevirtual #10 
 50 invokevirtual #11 
 53 astore 6
 55 new #8 
 58 dup
 59 invokespecial #9  : ()V>
 62 aload_1
 63 invokevirtual #10 
 66 aload_2
 67 invokevirtual #10 
 70 invokevirtual #11 
 73 astore 7

【例3】final变量拼接

场景3:final修饰的String变量,视作String常量

public static void test3() {
    final String s1 = "n";
    final String s2 = "b";

    String s3 = "nb";
    String s4 = "n" + "b"; // 编译期优化
    String s5 = s1 + "b";
    String s6 = "n" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4); // true
    System.out.println(s3 == s5); // true
    System.out.println(s3 == s6); // true
    System.out.println(s3 == s7); // true
    System.out.println(s5 == s6); // true
    System.out.println(s5 == s7); // true
    System.out.println(s6 == s7); // true
}

可以看到,我们只是在String变量前加上final,结果就完全不同了。

下面我们看一下class文件的反编译结果

  0 ldc #6 
  2 astore_0
  3 ldc #7 
  5 astore_1
  6 ldc #8 
  8 astore_2
  9 ldc #8 
 11 astore_3
 12 ldc #8 
 14 astore 4
 16 ldc #8 
 18 astore 5
 20 ldc #8 
 22 astore 6

可以看出,String变量被final修饰之后,所有的拼接操作都在编译期优化了,而没有使用StringBuilder

2、拼接小结

  • 常量常量拼接:
    • 拼接结果在字符串常量池,原理是编译期优化
  • 拼接中只要有一个是变量
    • 拼接结果在堆中,原理是 先new一个StringBuilder,然后用append()拼接,最后调用 toString() 返回结果
    • 补充说明:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
  • final修饰的String变量拼接:
    • 拼接结果在字符串常量池,仍然使用编译期优化,而非StringBuilder

因此,在开发中能使用上final的时候还是建议使用

3、+拼接 vs append拼接

public class StringAppendTest {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

//        String s1 = append1(100000); // 1670ms
        String s2 = append2(100000);   // 4ms

        long end = System.currentTimeMillis();
        System.out.println("拼接花费的时间为:" + (end - start));
    }

    public static String append1(int highLevel) {
        String str = "";
        for (int i = 0; i < highLevel; i++) {
            str = str + "a";  // 每次循环都会创建一个StringBuilder、String
        }
        return str;
    }

    public static String append2(int highLevel) {
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            strBuilder.append("a"); // 只需要创建一个StringBuilder
        }
        return strBuilder.toString();
    }
}

结论:通过StringBuilder的append()的方式拼接字符串的效率,远远高于String 使用 + 拼接

原因:

  • StringBuilder的append()的方式:
    • 自始至终中只创建过一个StringBuilder的对象。
  • String的+拼接方式:
    • 每一次字符串变量拼接的过程,都会new一个StringBuilder对象(这从之前的反编译结果中也可以看出来)

因此使用字符串变量+拼接会占用更大的内存,产生大量垃圾字符串,如果发生了GC,也会花费额外的时间。

4、StringBuilder的扩容

StringBuilder 空参构造器的初始化大小为16,超过该大小会进行扩容,涉及数组的copy操作

public StringBuilder() {    
    super(16);
}

如果提前知道需要拼接 String 的个数,就应该直接使用带参构造器指定capacity,以减少扩容的次数

public StringBuilder(int capacity) {    
    super(capacity);
}

5、优化小结

  • 允许的情况下尽量使用final。这样拼接操作会在编译期优化,而不会创建StringBuilder对象去append
  • 直接使用StringBuilderappend()效率要高于使用String的+拼接。
  • 如果知道最终的字符串长度,应该使用带容量的构造器创建StringBuilder,避免频繁扩容。

五、new String() 会创建几个对象

1、单独new

场景1:new String("ab") 会创建几个对象?(答案是2个)

 0: new           	#2                  // class java/lang/String
 3: dup
 4: ldc           	#3                  // String ab
 6: invokespecial 	#4                  // Method java/lang/String."":(Ljava/lang/String;)V
 9: astore_1
10: return
对象1  new String("ab")
对象2  常量池中的"ab"

2、常量 拼接 new

场景2:"a" + new String("b") 会创建几个对象?(答案是5个)

 0: new            	#2                  // class java/lang/StringBuilder
 3: dup
 4: invokespecial  	#3                  // Method java/lang/StringBuilder."":()V
 7: ldc            	#4                  // String a
 9: invokevirtual  	#5                  // Method java/lang/StringBuilder.append:
12: new           	#6                  // class java/lang/String
15: dup
16: ldc           	#7                  // String b
18: invokespecial 	#8                  // Method java/lang/String."":(Ljava/lang/String;)V
21: invokevirtual 	#5                  // Method java/lang/StringBuilder.append:
24: invokevirtual 	#9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: return
对象1	 new StringBuilder()
对象2  常量池中的"a"
对象3  new String("b")
对象4  常量池中的"b"   
对象5  StringBuilder.toString()new String("ab")

注意:StringBuilder.toString ,在字符串常量池中,没有生成 "ab"toString()的字节码指令中没有ldc

3、new 拼接 new

场景3:new String("a") + new String("b") 会创建几个对象?(答案是6个)

 0: new           	#2                  // class java/lang/StringBuilder
 3: dup
 4: invokespecial 	#3                  // Method java/lang/StringBuilder."":()V
 7: new           	#4                  // class java/lang/String
10: dup
11: ldc           	#5                  // String a
13: invokespecial 	#6                  // Method java/lang/String."":(Ljava/lang/String;)V
16: invokevirtual 	#7                  // Method java/lang/StringBuilder.append:
19: new           	#4                  // class java/lang/String
22: dup
23: ldc           	#8                  // String b
25: invokespecial 	#6                  // Method java/lang/String."":(Ljava/lang/String;)V
28: invokevirtual 	#7                  // Method java/lang/StringBuilder.append:
31: invokevirtual 	#9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
对象1	 new StringBuilder()
对象2  new String("a")
对象3  常量池中的"a"
对象4  new String("b")
对象5  常量池中的"b"   
对象6  StringBuilder.toString()new String("ab")

注意:常量池中没有"ab"

六、intern()

intern是一个native方法,调用的是底层C的方法

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    public native String intern();
}

1、intern() 的作用

调用intern()时,会判断 字符串常量池 中 是否已存在当前字符串(通过equals()方法判断)

  • 已存在:返回 字符串常量池 中 已存在的 该字符串对象的地址;
  • 不存在:将 该字符串 放入 字符串常量池,并返回 字符串对象的地址(这里jdk7前后略有不同)
    • JDK1.6中:会把此对象复制一份(新的引用地址),放入常量池,并返回新的引用地址
    • JDK1.7起:会把此对象的引用地址复制一份(相同的引用地址),放入常量池,并返回此对象的引用地址

也就是说,任意字符串调用intern(),其返回结果所指向的那个类实例,必定和直接以常量形式出现的字符串实例完全相同。

 ("a"+"b"+"c").intern() == "abc" // true;

intern()可以确保字符串在内存里只有一份(即字符串常量池中的),可以节约内存空间,加快字符串操作任务的执行速度。

2、案例分析

1)案例1

public void test() {
    String s1 = new String("a");
    s1.intern();
    String s2 = "a";
    System.out.println(s1 == s2);  // jdk6 false ; jdk7/8 false
}
  • s1记录的是堆中new String("a")的地址
  • s2记录的是字符串常量池中"a"的地址

2)案例2

public void test() {
    String s1 = new String("a") + new String("b");
    s1.intern();
    String s2 = "ab";
    System.out.println(s1 == s2);  // jdk6 false ; jdk7/8 true
}
  • s1记录的是堆中"ab"的地址(注意,这个"ab"StringBuilder.toString()生成的,没有往常量池里放)
  • s1.intern()调用这个方法之前,字符串常量池中并不存在"ab",所以要把"ab"放入字符串常量池
    • jdk6中:字符串常量池中的"ab"指向新的地址。
    • jdk7起:字符串常量池中的"ab"指向的是调用intern()s1的地址
  • s2记录的是字符串常量池中的"ab"指向的地址

3)案例3

public void test() {
    String s1 = new String("a") + new String("b");
    String s2 = s1.intern();		 // 常量池没有"ab",会放入
    System.out.println(s1 == "ab");  // jdk6 false ; jdk7/8 true
    System.out.println(s2 == "ab");  // jdk6 true  ; jdk7/8 true       
}
public void test() {
        String s1 = "ab";			   // 常量池中创建一个新的对象"ab"
        String s2 = new String("a") + new String("b");
        String s3 = s2.intern();	   // 常量池已有"ab",不会再放入
        System.out.println(s1 == s2);  // jdk6/7/8  false
        System.out.println(s1 == s3);  // jdk6/7/8  true     
}

3、intern() 的效率:空间角度

public class StringInternTest {    
    static final int MAX_COUNT = 1000 * 10000;    
    static final String[] arr = new String[MAX_COUNT];   
    
    public static void main(String[] args) {        
        Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};      
        
        long start = System.currentTimeMillis();  
        for (int i = 0; i < MAX_COUNT; i++) {            
// 			arr[i] = new String(String.valueOf(data[i%data.length]));    		 // 不用intern   7256ms    
            arr[i] = new String(String.valueOf(data[i%data.length])).intern();   // 使用intern   1395ms
        }       
        long end = System.currentTimeMillis();   
        System.out.println("花费的时间为:" + (end - start));    
        
        try {           
            Thread.sleep(1000000);      
        } catch (Exception e) {      
            e.getStackTrace();    
        }  
        System.gc();
    }
}
  • 直接new:堆中 和 字符串常量池中可能会存在相同的字符串。
  • 使用intern():保证内存中相同的字符串只会有一个,就在字符串常量池中。

结论:对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern()方法能够节省内存空间。

七、StringTable 的垃圾回收

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

先空跑一下,看一下StringTable statistics的信息

public class StringGCTest {
    public static void main(String[] args) {

    }
}
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1277 =     30648 bytes, avg  24.000
Number of literals      :      1277 =    100808 bytes, avg  78.941

然后循环添加String对象,再看一下StringTable statistics的信息

public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 1000; j++) {
            String.valueOf(j).intern();
        }
    }
}
// 这里可以看到Number大概增长了1000
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      2258 =     54192 bytes, avg  24.000
Number of literals      :      2258 =    147896 bytes, avg  65.499
Total footprint         :           =    682192 bytes

最后我们把循环次数调大一点,使其发生GC

public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}

字符串常量池_第8张图片

这里我们可以看到 PSYoungGen 区发生了垃圾回收

// 这里也可以看到Number没有达到循环次数100000
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     63891 =   1533384 bytes, avg  24.000
Number of literals      :     63891 =   3607296 bytes, avg  56.460
Total footprint         :           =   5620784 bytes

以上两点都说明了 StringTable 发生了垃圾回收

八、G1 中的 String 去重操作

1、背景

注意:不是字符串常量池的去重操作,字符串常量池本身就没有重复的字符串

背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:

  • 堆存活数据集合里面String对象占了25%
  • 堆存活数据集合里面重复的String对象有13.5%(差不多一半String对象是重复的)
  • String对象的平均长度是45

堆上存在大量重复的String对象必然是一种内存的浪费。对重复的String对象进行去重,就能避免浪费内存。

2、实现

  1. 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。

  2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。

    处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。

  3. 使用一个Hashtable来记录所有的被String对象使用的不重复的char数组。

    当去重的时候,会查这个Hashtable,来看堆上是否已经存在一个一模一样的char数组。

  4. 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。

  5. 如果查找失败,char数组会被插入到Hashtable,这样以后的时候就可以共享这个数组了。

3、命令行选项

  1. UseStringDeduplication(bool) :开启String去重,默认是不开启的,需要手动开启。
  2. PrintStringDeduplicationStatistics(bool) :打印详细的去重统计信息
  3. StringDeduplicationAgeThreshold(uintx) :达到这个年龄的String对象被认为是去重的候选对象

你可能感兴趣的:(JVM,java,开发语言)