要验证字符串去重效果,必须开启-xx:+printstringdeduplicationstatistics等三项日志参数,通过gc.log中“string deduplication”行的strings、processed、deduplicated、skipped四指标及内存压缩数据(如4215k→382k)综合评估。

要真正看清字符串去重有没有在干活、干了多少活、省了多少内存,不能靠猜,得靠日志——而且是带明确指标的 GC 日志。
必须开启的三个日志参数
单开 -XX:+UseStringDeduplication 不会自动输出任何可观测数据。你需要显式组合以下参数启动 JVM:
- -XX:+PrintStringDeduplicationStatistics:这是核心,让 G1 每次执行混合 GC(Mixed GC)或 Full GC 后,打印一行去重统计
- -Xloggc:gc.log 或 -Xlog:gc*:file=gc.log(JDK 9+ 推荐):把日志落地到文件,避免刷屏丢失
- -XX:+PrintGCDetails(JDK 8)或保留 -Xlog:gc,gc+stringdedup=debug(JDK 11+):确保去重行不被过滤掉
怎么看日志里那行关键输出
在 gc.log 中搜索 String Deduplication,典型行如下:
[String Deduplication: 24891 strings, 18302 processed, 15673 deduplicated (85.6%), 2629 skipped]重点关注四个数字:
- strings:本轮 GC 扫描范围内发现的 String 对象总数(仅老年代)
- processed:实际参与比对的个数——若长期为 0,说明字符串根本没活到老年代,需检查晋升策略
- deduplicated:成功合并的数量,除以 processed 就是去重成功率
- skipped:跳过的原因通常是年龄不足(默认需经历 3 次 Minor GC 才触发去重),可调 -XX:StringDeduplicationAgeThreshold=2 加速
结合内存变化看真实收益
去重节省的是底层数组空间,不是对象头。所以光看对象数量下降不够,要看字节级压缩效果:
- 日志中还会出现类似 [String Deduplication: 4215K->382K (3833K)] 的行,表示“原占 4215KB,去重后只剩 382KB,净省 3833KB”
- 配合堆直方图对比:用 jmap -histo:live
查看 before/after 的 java.lang.String 实例数和总 KB 数,验证是否同步下降 - 观察老年代使用率趋势:开启后若 CMS Initiated Occupancy Fraction 或 G1MixedGCLiveThresholdPercent 对应的水位更平稳,说明去重缓解了碎片压力
高频误判点与避坑提示
别被表面数字误导:
- 看到 Deduplicated: 0 ≠ 功能失效,可能是字符串生命周期太短(如 HTTP 请求中临时拼接的 path),还没进老年代就被回收了
-
jcmd
VM.stringtable 显示的 entry 数跟去重无关——那是 intern() 引用表,G1 去重绕过 StringTable,直接操作堆内 char[]/byte[] - 日志里 attempted 为 0?检查是否用了 ZGC/Shenandoah——它们不支持该特性,必须用 G1










