Java中String内存泄漏主因是intern()致常量池膨胀:JDK7+池在堆中但难回收,需用jstat/jmap/jcmd定位异常增长与调用点,禁用非必要intern,改用去重缓存或JDK21字符串去重。

Java 中 String 相关的内存泄漏,多数不是“String 对象本身泄漏”,而是因不当使用 intern() 导致字符串常量池(StringTable)持续膨胀、难以回收,最终拖垮堆内存。排查关键不在于找“泄露的 String”,而在于确认常量池是否异常增长、哪些字符串被反复 intern、是否本不该进池。
看常量池实际大小和增长趋势
常量池在 JDK 7+ 后位于堆中(老年代),可被 GC 回收,但前提是池中字符串不再被任何地方强引用。若发现老年代持续上涨、Full GC 后仍不回落,且 String 类实例数居高不下,就要怀疑 StringTable。
- 用
jstat -gc <pid>观察OU(Old Used)和OC(Old Capacity)变化,配合-XX:+PrintGCDetails看 Full GC 前后老年代回收效果 - 用
jmap -histo:live <pid> | grep java.lang.String统计存活 String 实例数;再执行jmap -finalizerinfo <pid>排除 finalizer 阻塞 - 启用
-XX:+PrintStringDeduplicationStatistics(仅 G1 GC)可观察字符串去重效果,间接反映重复字符串规模
定位哪些字符串进了常量池
不能只靠猜——要实锤是哪些字符串被 intern、调用来源在哪。
- 用
jcmd <pid> VM.native_memory summary scale=MB查看StringTable区域内存占用(JDK 8+ 支持) - 开启
-XX:+TraceClassLoading和-XX:+TraceClassUnloading辅助判断类加载是否异常,因为某些框架(如 Jackson)会在反序列化 Map 时对 key 自动 intern - 结合
jstack <pid>抓线程快照,搜索intern、StringTable、g1StringTable等关键词,定位高频调用点(例如日志框架、配置解析、HTTP header 处理)
验证 intern 是否真有必要
很多场景下调用 intern() 是画蛇添足:用户输入、JSON 字段名、URL 路径拼接等短生命周期字符串,进池后反而延长存活时间,挤占老年代空间。
立即学习“Java免费学习笔记(深入)”;
- 检查代码中所有
.intern()调用点,问三个问题:该字符串是否长期复用?是否作为 key 高频参与switch或HashMap查找?是否内容高度重复(如固定状态码、枚举字面量)? - 临时字符串拼接(如
"user_" + id)或带时间戳的路径(如"/api/v1/log/" + System.currentTimeMillis())绝不应 intern - 若必须缓存字符串,优先考虑
ConcurrentHashMap<String, WeakReference<String>>或 Caffeine 缓存,而非依赖常量池
修复与规避策略
一旦确认是 StringTable 膨胀引发问题,修复不是删掉 intern 就完事,而是分层应对。
- 紧急止血:加 JVM 参数
-XX:StringTableSize=60013(质数,避免哈希冲突),默认值太小(1009)会导致链表过长,加剧 GC 压力 - 长期治理:升级到 JDK 21+,启用
-XX:+UseStringDeduplication(G1)自动合并重复字符串,替代手动 intern - 监控兜底:在 Prometheus + Grafana 中接入
sun.gc.stringtable.*JMX 指标(如sun.gc.stringtable.size、sun.gc.stringtable.capacity),设置告警阈值


















