1. String的基本特性
1.1 JDK9存储结构变革
技术演进说明:
- JDK8及之前:使用
<font style="background-color:rgb(252, 252, 252);">char[]</font>
存储(每个字符2字节) - JDK9+引入Compact Strings:
<font style="background-color:rgb(252, 252, 252);">byte[] value</font>
+<font style="background-color:rgb(252, 252, 252);">byte coder</font>
组合存储- LATIN1编码(单字节)自动应用于纯英文字符串
- UTF16编码(双字节)用于包含多字节字符的情况
- 实测内存节省30%~50%
1.2 核心特性解析
关键特性详解:
- 不可变性(Immutable)
String s1 = "java";
s1 = s1.concat("8"); // 实际产生新对象
- 每次修改生成新对象,原对象保持不可变
- 优势:线程安全、哈希码缓存(
<font style="background-color:rgb(252, 252, 252);">private int hash</font>
)
- Final类保护
- 禁止通过继承破坏不可变机制
- 字符串常量池(String Table)
- 全局共享的字面量存储池
- 位置演变:
2. String内存分配机制
2.1 双模式分配原理
内存分配规则:
- 字面量赋值
String s1 = "java"; // 直接入池
- 首次出现时创建并存入StringTable
- 后续相同字面量直接复用
- New关键字创建
String s2 = new String("java");
- 强制在堆中创建新对象
- 字符数组value指向常量池对象
2.2 内存布局示例
关键参数配置:
-XX:StringTableSize=60013 # 调整池大小(素数优化哈希碰撞)
-XX:+PrintStringTableStatistics # 输出统计信息
3. String基本操作原理
3.1 对象创建与比较
核心操作规则:
- 字面量赋值优先使用常量池
<font style="background-color:rgb(252, 252, 252);">new String()</font>
强制堆内存分配<font style="background-color:rgb(252, 252, 252);">==</font>
比较对象地址,<font style="background-color:rgb(252, 252, 252);">equals</font>
比较字符序列
3.2 方法调用影响
String str = "Hello";
str = str.concat("World"); // 产生新对象
str = str.substring(0,5); // 可能共享原数组
str = str.replace('H','h'); // 创建新数组
方法特性说明:
<font style="background-color:rgb(252, 252, 252);">substring</font>
:JDK7u6前共享原数组,之后复制新数组<font style="background-color:rgb(252, 252, 252);">replace</font>
:始终创建新数组<font style="background-color:rgb(252, 252, 252);">toLowerCase</font>
:涉及本地方法调用,性能敏感
4. 字符串拼接机制
4.1 编译器优化规则
字节码验证示例:
// 源代码
String s = "a" + "b";
String s2 = s + new String("c");
// 反编译后
String s = "ab";
String s2 = new StringBuilder().append(s).append(new String("c")).toString();
4.2 堆内存变化分析
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
对象创建统计:
- 常变量混合拼接:至少产生3个对象(StringBuilder、拼接结果、可能的临时对象)
- 循环拼接场景:产生大量临时StringBuilder和String对象
5. intern()深度解析
5.1 JDK版本差异对比
// JDK6示例
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2); // false
// JDK8示例
String s3 = new String("2");
s3.intern();
String s4 = "2";
System.out.println(s3 == s4); // true
版本差异总结表:
特性 | JDK6 | JDK7/8 |
---|---|---|
字符串池位置 | 永久代 | 堆内存 |
intern()返回值 | 永久代对象引用 | 堆中首次出现对象引用 |
内存溢出风险 | 永久代OOM | 堆内存OOM |
5.2 空间优化测试
public class InternTest {
static final int MAX = 1000000;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < MAX; i++) {
// 测试不同模式
String s = String.valueOf(i).intern();
list.add(s);
}
}
}
测试结论:
- 使用intern后内存减少80%+
- StringTable默认大小60013,需根据场景调整:
-XX:StringTableSize=1000003 # 设置为大于数据量的素数
6. StringTable垃圾回收机制
6.1 回收触发条件
核心回收机制:
- 存活判定标准
- 无任何GC Roots引用的字符串对象(包括失去所有引用的字面量)
- StringTable内部维护的引用不作为GC Root
- 回收触发场景
监控方法:
# 启动参数
-XX:+PrintStringTableStatistics
-XX:+PrintGCDetails
# 输出示例
StringTable statistics:
Number of buckets : 60013
Average bucket size : 2.0
6.2 各收集器处理差异
收集器对比表:
收集器类型 | StringTable处理特点 | 适用场景 |
---|---|---|
Serial | Full GC时全量扫描 | 客户端小内存应用 |
CMS | 并发周期不处理,Full GC时处理 | 大堆低延迟系统 |
G1 | Mixed GC阶段增量处理 | 超大堆内存环境 |
ZGC | 并发标记阶段处理 | 超低延迟需求 |
7. G1的字符串去重优化
7.1 去重原理图解
去重三阶段流程:
- 标记阶段:识别重复候选(连续多次Young GC存活)
- 哈希计算:对char[]内容计算哈希值
- 去重处理:通过全局哈希表合并相同字符串
7.2 启用与调优
# 基础参数
-XX:+UseG1GC
-XX:+UseStringDeduplication
# 高级调优
-XX:StringDeduplicationAgeThreshold=3 # 默认3次GC后处理
性能影响测试数据:
字符串重复率 | 内存节省 | 暂停时间增加 |
---|---|---|
30% | 25% | 2ms |
60% | 40% | 5ms |
90% | 65% | 8ms |
8. 常见问题与解决方案
8.1 问题排查表
现象 | 根本原因 | 解决方案 |
---|---|---|
PermGen OOM | JDK6下大量intern操作 | 升级JDK7+,调整-XX:MaxPermSize |
堆内存持续增长 | 未合理使用intern或拼接操作 | 1. 检查循环拼接逻辑 2. 增加StringTableSize |
Young GC时间过长 | 大StringTable导致扫描耗时 | 1. 使用G1收集器 2. 启用字符串去重 |
应用启动慢 | 初始StringTable过小导致频繁rehash | 预设置合适的-XX:StringTableSize |
8.2 内存泄漏案例
// 错误示例:持续将随机字符串入池
List<String> list = new ArrayList<>();
while(true) {
String uuid = UUID.randomUUID().toString().intern();
list.add(uuid);
}
// 正确写法:避免非重复值intern
String predefine = "User_";
int counter = 0;
public String generate() {
return (predefine + counter++).intern();
}
9. 高频面试题精析
Q1:String s = new String("abc")创建几个对象?
答案:
- 当"abc"首次出现时:
- 常量池对象(1个)
- 堆中String实例(1个)\ 总计2个
- 已存在常量池时:
- 仅堆中实例(1个)
Q2:intern()在不同JDK版本中的区别?
答案对比:
Q3:如何高效处理百万级字符串?
优化方案:
- 预入池已知值
private static final String CACHE_PREFIX = "ID_";
public String generate(int id) {
return CACHE_PREFIX + id;
}
- 配置参数调优
-XX:StringTableSize=1000003 # 使用素数减少哈希碰撞
- 启用G1去重
-XX:+UseG1GC -XX:+UseStringDeduplication
Q4:StringTable性能调优关键点?
调优矩阵:
参数 | 影响范围 | 推荐值 |
---|---|---|
StringTableSize | 哈希桶数量 | 应用实例数的2~3倍 |
StringDeduplicationAgeThreshold | 去重延迟 | 根据Young GC频率调整 |
G1ConcStringTableCleanupInterval | G1清理间隔 | 默认2ms不修改 |