String Deduplication 是 G1 垃圾收集器专属的运行时字符串去重机制,通过在 GC 中合并重复的 String 底层数组来节省内存,但仅对老年代中内容完全相同的 char[]/byte[] 生效,需满足 G1、显式启用及 JDK≥8u20 三条件,且效果依赖字符串重复率与生命周期。

String Deduplication 是什么,它真能省内存?
它不是 JVM 的通用字符串池优化,而是 G1 垃圾收集器专属的运行时去重机制:在 GC 过程中扫描堆上重复的 String 对象(仅限底层 char[] 或 byte[] 内容完全相同),保留一个实例,让其余对象指向它。效果取决于字符串重复率和生命周期——堆里有大量长生命周期、内容雷同的字符串(比如日志 ID、JSON 字段名、HTTP Header 值)时,内存节省才明显;如果字符串大多短命或唯一,开销反超收益。
怎么开启并确认 String Deduplication 生效?
必须同时满足三个条件才能启用:
- 使用 G1 收集器:
-XX:+UseG1GC - 显式启用去重:
-XX:+UseStringDeduplication(默认关闭) - JDK 版本 ≥ 8u20(JDK 9+ 默认仍关闭,需手动加参数)
验证是否生效,看 GC 日志里的 StringDeduplication 行:
[GC pause (G1 Evacuation Pause) (young), 0.023 ms] [String Deduplication: 0.000 ms, 0 processed, 0 deduplicated, 0 attempted]
注意:processed 不为 0 才说明扫描已启动;deduplicated 是真正合并的数量。如果长期为 0,大概率是字符串还没活过第一个 GC 周期(去重只作用于老年代对象),或内容实际不重复。
为什么开了却没看到内存下降?常见踩坑点
去重本身不立即释放内存,它只是把多个 String 的底层数组引用指向同一份数据,原数组变成垃圾等下次 GC 回收。所以观察窗口要拉长,且得看老年代占用趋势(用 jstat -gc 看 OU 列)。
- 字符串太“新”:G1 只对晋升到老年代的
String做去重,年轻代对象直接忽略 - 用了
String.intern():它走的是 JVM 字符串常量池(PermGen / Metaspace),和 String Deduplication 完全无关,二者不协同也不冲突 - 字符串底层是
byte[]但编码不同:JDK 9+ 的紧凑字符串(coder字段)要求coder和内容都一致才去重;Latin-1和UTF-16编码的相同文本不会被识别为重复 - 启用了
-XX:+UseCompressedOops(默认开启)但堆 > 32GB:可能导致对象头布局变化,间接影响去重扫描效率(罕见,但高内存场景可排查)
性能代价和调优参数有哪些?
去重在 GC 暂停期间执行,会略微延长 young GC 时间(尤其老年代大、重复字符串多时)。它用哈希表维护已见数组指纹,默认最大 1M 条目,满后开始驱逐旧条目。
- 控制哈希表大小:
-XX:StringDeduplicationTableSize=1048576(默认值,不建议乱调) - 调整扫描频率(高级):
-XX:StringDeduplicationAgeThreshold=3表示对象至少经历 3 次 GC 后才参与去重(默认为 3,提高阈值可减少 young 区误扫) - 监控开销:
-XX:+PrintStringDeduplicationStatistics输出详细统计,但别在生产长期开着——日志量不小
真正关键的取舍在于:你愿不愿意为可能的内存节约,承担一点点 GC 时间波动。没有银弹,只有权衡。










