-XX:+PrintStringTableStatistics 输出字符串表统计信息而非常量池,用于定位因 String.intern() 泛滥导致的 GC 标记开销问题:需关注 entry 数量、桶均值/最大值及内存占用,并结合 GC 日志、jmap 和 JDK 升级等手段优化。

-XX:+PrintStringTableStatistics 并不直接打印常量池(Constant Pool)信息,而是输出 JVM 字符串表(String Table,即 interned string 的哈希表)的统计信息。它和“长周期常量池拖累 GC”之间存在常见误解:Java 的运行时常量池(Runtime Constant Pool)本身不参与 GC 扫描;真正影响 GC 性能的是大量被 String.intern() 进入字符串表的长生命周期字符串——它们驻留在堆外(JDK 7+ 在堆内)但长期存活,导致 GC 需要反复扫描、标记和处理这些对象。
理解字符串表与 GC 的真实关联
字符串表本质是堆内一个全局哈希表(StringTable),每个 interned String 对象本身是普通 Java 对象,存于堆中;它的 key 是字符串内容,value 是堆中对应的 String 实例引用。GC(尤其是 CMS、G1、ZGC)在并发标记或 STW 标记阶段,必须遍历该表中的所有 entry,对每个 String 实例做可达性分析。当表中 entry 数量达百万级、且大量 String 持有大 char[]/byte[] 时,会显著增加标记耗时和内存压力。
如何用 PrintStringTableStatistics 定位问题
启用该参数后,JVM 在退出或 Full GC 后打印类似如下信息:
StringTable statistics: Number of buckets : 65536 Number of entries : 2843210 Number of literals : 2843210 Mean bucket size : 43.39 Max bucket size : 127 Maximum probe distance : 22 Total footprint : 89532160 bytes
重点关注三项:
-
Number of entries:若持续 > 100 万,说明 intern 泛滥,需检查业务是否滥用
String.intern()(如解析 JSON/CSV 时对字段名、枚举值盲目 intern) - Mean / Max bucket size:均值 > 10 或最大桶长 > 50,表明哈希冲突严重,可能因字符串内容高度相似(如带时间戳的固定前缀),加剧遍历开销
- Total footprint:单位字节,若超百 MB,说明字符串对象及其底层数组占用巨大堆空间,且基本不回收
结合其他工具交叉验证
单靠该日志不够,需联动分析:
- 开启 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps,比对 GC 日志中标记阶段(如 G1 的 “Concurrent Mark” 或 ZGC 的 “Mark”)耗时是否随字符串表膨胀同步增长
- 用 jmap -histo:live <pid> 查看 top 3 类型是否为
java.lang.String和[C(char[])或[B(byte[]),确认其实例数和容量占比 - JDK 8u262+ 可配合 -XX:+PrintStringDeduplicationStatistics(若启用了字符串去重),区分 intern 和去重引入的冗余
针对性优化建议
- 禁用非必要
String.intern():尤其避免在循环、高频解析逻辑中调用;改用ConcurrentHashMap<String, String>做可控缓存,并设置合理过期策略 - 缩小字符串表初始大小(谨慎):通过 -XX:StringTableSize=65536(默认值)可调小,但过小会加剧哈希冲突;建议先压测再调整
- 升级 JDK:JDK 12+ 对 StringTable 并发访问和清理做了优化;JDK 17+ 的 ZGC 在字符串表遍历上进一步减少暂停影响
- 监控落地:将
Number of entries指标接入 Prometheus(可用 JMX Exporter 抓取java.lang:type=StringPoolMBean),设置告警阈值(如 > 50 万)

















