JVM--StringTable

JVM–StringTable

背景

  • JVM–StringTable

  • 博主以黑马JVM进行学习

核心概念

  • Class 常量池(编译期)→ 运行时常量池(JVM 加载后)→ 串池(StringTable,运行时)
    • Class 常量池:.class文件里存储字面量、符号引用的区域(比如代码里的"a" "b" "ab"编译后是符号,不是对象);
    • 运行时常量池:Class 常量池加载到 JVM 内存后的区域(符号仍未变成 Java 对象)
    • 串池(StringTable):JVM 专门存储字符串对象的哈希表(hashtable),属于堆内存的一部分,只有触发特定操作(如ldc指令),运行时常量池的符号才会变成字符串对象存入串池
      • JDK 1.6:属于方法区(永久代)
      • JDK 1.7+:移至堆内存
      • 核心触发逻辑:只有执行ldc指令(访问字符串字面量)、调用intern()方法时,运行时常量池的符号才会转为字符串对象存入串池。
  • 特性:
    • 常量池中的字符串仅仅是符号,第一次用到时才变为对象
    • 利用串池的机制,来避免重复创建字符串对象
    • 字符串变量拼接的原理是StringBuilder(1.8)
    • 字符串常量拼接的原理是编译期优化,直接从串池取对象
    • 可以使用intern方法,主动将串池还没有的字符串对象放入串池
      • 1.8:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
      • 1.6:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回

常量池与串池的关系

  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //StringTable在数据结构中是哈希表hashtable结构,不能扩容
    //StringTable[ "a","b","ab"]
    public class demo {
    //常量池中的信息,都会加载到运行时常量池中,这时a,b,ab都是常量池中的符号,还没变为java字符串对象
    //ldc #2 会把a符号变为“a”字符串对象

    public static void main(String[] args){
    String s1 = "a"; //懒惰的机制 核心:触发ldc指令,符号→对象,存入串池
    String s2 = "b";
    String s3 = "ab";
    }

    }
  • 代码执行的底层逻辑:

    • 类加载阶段:demo.class 的 Class 常量池被加载到 JVM 的运行时常量池,此时"a" "b" "ab"只是「符号引用」(类似占位符),没有创建任何 String 对象;

    • 执行String s1 = "a"

      • JVM 执行ldc #2指令(#2 是运行时常量池中"a"的符号引用);
      • 触发「懒惰机制」:检查串池(StringTable)中是否有"a"对象;
      • 若没有,创建"a"字符串对象,存入串池;
      • 把串池中"a"对象的引用赋值给 s1;
    • 执行String s2 = "b" String s3 = "ab":逻辑和s1完全一致 —— 先查串池,无则创建对象存入,再返回引用。

字符串变量拼接

  • 对比

    拼接类型 底层实现 是否入串池 示例 结果说明
    常量拼接 编译期优化(直接合并) String s5 = "a"+"b" 等价于String s5 = "ab",取串池对象
    变量拼接 StringBuilder.append()+toString() String s4 = s1+s2 生成新 String 对象(堆中),不在串池
  • “符号” vs “对象” 的核心区别

    • 符号:运行时常量池中的占位符,仅记录字符串内容,不是 Java 对象,不占堆内存;

    • 对象:触发ldc指令后,在串池中创建的String实例,有内存地址、可调用方法(如equals())。

    • 懒惰机制:

      1
      2
      3
      //new StringBuilder().append("a").append("b").toString()  new String("ab")
      String s4 = s1 + s2; // 运行时拼接,创建新的String对象(不在串池,除非手动intern())
      System.out.println(s3==s4)//返回false

编译期优化

1
2
3
4
// 反例:编译期常量拼接,直接在Class常量池生成"ab",运行时无需拼接
String s5 = "a" + "b"; // 编译后等价于String s4 = "ab",直接从串池取"ab"
//javac在编译期间的优化,结果已经在编译期间确定为ab
System.out.println(s3==s5) //返回true

字符串延迟加载

  • 字符串字面也是【延迟】成为对象的

  • 使用jmap的Memory查看每类对象的实例个数

  • 执行一行代码后才放入串池

intern_1.8

  • 案例

    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
    public class demo {
    //["a","b","ab"]
    public static void main(String[] args){
    String s = new String("a") + new String("b"); // new String("ab")

    //堆 new String("a") new String("b") new String("ab")
    String s2 = s.intern(); //将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    System.out.println(s2 == "ab");//返回true
    System.out.println(s == "ab");//返回true
    }

    }



    public class demo1 {
    //["ab","a","b"]
    public static void main(String[] args){
    String x = "ab";
    String s = new String("a") + new String("b"); // new String("ab")
    //堆 new String("a") new String("b") new String("ab")
    String s2 = s.intern(); //将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    System.out.println(s2 == "ab");//返回true
    System.out.println(s == "ab");//返回false
    }

    }

intern_1.6

  • 案例

    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
    public class demo {
    //["ab","a","b"]
    public static void main(String[] args){
    String x = "ab";
    String s = new String("a") + new String("b"); // new String("ab")
    //堆 new String("a") new String("b") new String("ab")
    String s2 = s.intern(); //将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
    System.out.println(s2 == "ab");//返回true
    System.out.println(s == "ab");//返回false
    }
    }



    public class demo1 {
    //["a","b","ab"]
    public static void main(String[] args){

    String s = new String("a") + new String("b"); // new String("ab")
    //堆 new String("a") new String("b") new String("ab")
    String s2 = s.intern(); //将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
    //s复制一份,放入串池

    String x = "ab";
    System.out.println(s2 == "ab");//返回true
    System.out.println(s == "ab");//返回false
    }
    }

位置

JDK 版本 存储位置 所属内存区域
1.6 方法区(永久代) 堆内存子集
1.7+ 堆内存(独立区域) 堆内存

垃圾回收

  • 串池中的字符串对象并非永久存活,满足以下条件会被 GC:
    • 串池中的对象无任何外部引用(如字面量不再被使用);
    • JVM 触发堆 GC(如 Minor GC/Major GC)时,会清理串池中无引用的字符串对象。

调优

  • 如果StringTable中字符串常量过多的话,可以使用-XX:StringTableSize=桶个数,将桶的数量设置多
  • 考虑将字符串对象是否入池
  • JDK 1.6 中StringTableSize默认 1009,JDK 1.8+ 默认 60013,且 1.8+ 容量设置后不可动态修改。

总结

  • StringTable 是存储字符串对象的哈希表,JDK 1.7+ 移至堆内存,核心作用是字符串去重;
  • 字符串字面量(如 “a”)是 “延迟实例化” 的符号,第一次使用时(ldc 指令)才转为对象入串池;
  • 常量拼接编译期优化(入串池),变量拼接底层是 StringBuilder(不入串池);
  • intern() 方法:JDK 1.8 放入对象引用,JDK 1.6 复制对象入池,核心是复用串池对象减少内存占用;
  • 调优关键:调整 StringTableSize 减少哈希冲突,对重复字符串主动调用 intern()