JVM StringTable

String类的基本特性

  • 不可变的, 不可被继承的(final类), 支持序列化(实现了 Serializble接口), 可以比较大小(实现了 Comparable接口)

两种定义方式

  1. 字面量方式: 也就是直接使用双引号括起字符串的声明方式
  • 此方式会将 String对象直接存储在字符串常量池中

String s1 = "abc";

  1. 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性能

字符串拼接操作

  1. 常量与常量的拼接结果在常量池, 原理是编译期优化(通过字节码指令可以确认)
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是变量, 结果就会存到对象实例中, 变量拼接是通过 StringBuilder实现的
  4. 如果拼接的结果调用 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”); 会创建几个对象?
  1. 一个是在堆上创建的实例
  2. 再一个是常量池里加一个"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”); 会创建几个对象?
  1. 0: new StringBuilder 创建 StringBuilder的实例
  2. 7: new String 创建 String的实例
  3. 11: ldc a 创建"a"的常量, 同时 StringBuilder.append(“a”)
  4. 19: new String 创建 String的实例
  5. 23: ldc b 创建"b"的常量, 同时 StringBuilder.append(“b”)
  6. 再深入就会 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次)

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页