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()方法时,运行时常量池的符号才会转为字符串对象存入串池。
- Class 常量池:
- 特性:
- 常量池中的字符串仅仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是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;
- JVM 执行
-
执行
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 | // 反例:编译期常量拼接,直接在Class常量池生成"ab",运行时无需拼接 |
字符串延迟加载
-
字符串字面也是【延迟】成为对象的
-
使用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
27public 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
28public 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()。