排查优化重复String的核心思路是:先用jmap/JFR等工具确认真实冗余,再筛选内容固定、重复率高、生命周期长的字符串(如状态码、HTTP方法),最后按场景选intern()、ConcurrentHashMap缓存或G1去重。

排查和优化大量重复 String 的核心思路是:先确认是否真存在冗余,再判断哪些字符串值得去重,最后选择合适手段——不是所有重复都该用 intern(),也不是所有场景都适合自动去重。
一、确认是否存在真实重复(别凭感觉)
重复字符串是否造成内存压力,必须靠工具验证:
- 用
jmap -histo查看java.lang.String实例数量和 retained heap 大小,对比业务高峰期前后的变化 - 用 VisualVM 或 JFR 抓取堆快照,按内容分组(Group by Value),筛选出高频出现的字符串(如 "PENDING" 出现 5 万次)
- 启用 G1 字符串去重统计:
-XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics,观察日志中 deduplicated 字节数是否显著增长
二、识别哪些字符串适合优化
关键看三个条件:内容固定、重复率高、生命周期长。典型例子包括:
- 从数据库或 JSON 中读取的状态字段("SUCCESS"、"FAILED"、"CREATED")
- HTTP 方法、日志级别、协议头名("GET"、"ERROR"、"Content-Type")
- 枚举常量名、反射获取的方法/字段名(
method.getName().intern())
以下情况不要 intern:
- UUID、时间戳、随机 token 等唯一值
- 拼接生成的临时字符串(如
"UPDATE t SET v=" + value) - 字面量本身(
"hello".intern()无意义,它已在池中) - 超短(如 "Y"/"N")或超长(>1KB)字符串,收益低甚至负向
三、选对优化方式,避免踩坑
方案1:谨慎使用 String.intern()
- 只在明确知道重复且稳定时调用,例如:
status = row.getString("status"); return status == null ? null : status.intern(); - 绝对禁止在循环内无条件调用,尤其配合分页查询、日志解析等批量逻辑
- JDK 7+ 后常量池在堆中,但仍是全局同步操作,高并发下会成为瓶颈
方案2:显式缓存(更推荐)
- 用
ConcurrentHashMap自建字符串池,可按业务维度清理(如一次导入任务结束后cache.clear()) - 启动时预加载白名单:读取全部合法状态值 → 去重 →
intern()并缓存引用,运行时只查不写 - 规避 JVM 全局锁,生命周期可控,便于监控和回收
方案3:启用 G1 字符串去重(被动兜底)
- 需加参数:
-XX:+UseG1GC -XX:+UseStringDeduplication - 仅处理存活超过 3 次 GC 的字符串(默认阈值),短命对象不参与
- 不能替代主动优化,只是辅助手段;若依赖它来“救火”,说明代码已有设计缺陷
四、验证优化是否生效
改完不能只看日志没报错,要确认效果:
- 对比优化前后
jmap -histo中java.lang.String的实例数,下降 30% 以上才算有效 - 观察 Full GC 频率是否降低,尤其是老年代占用增长变缓
- 用 JFR 录制一段时间,检查字符串相关内存分配事件是否减少
不复杂但容易忽略。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











