根本原因是强引用缓存导致对象无法回收,应改用intern()复用、枚举或id映射,并为强引用缓存设置容量限制与淘汰机制。

这个问题本质是强引用阻断了 GC 回收路径,加上字典数据长期驻留、重复率高但未做对象复用,最终堆内存持续攀升直至 OOM。关键不在于“要不要缓存”,而在于“怎么缓存才既安全又高效”。
明确字典字段的生命周期和取值特征
不是所有字典都适合强引用缓存。先确认:
- 该字段实际取值是否真的有限(比如“性别”只有“男/女/未知”,“订单状态”稳定在 10 种以内)
- 这些值是否在系统运行期间基本不变(上线后极少增删改)
- 是否被高频访问、且每次访问都 new 出新 String 或新 VO 对象
如果答案都是“是”,那问题不在缓存本身,而在缓存方式——当前用纯强引用 List/Map 存原始对象,等于把百万条记录的每个字符串都钉死在堆里。
用 intern() 做轻量级字符串复用(仅限稳定字典值)
对内容固定、重复极高的字符串字段(如 status_code、category_code),启动时预加载并显式调用 intern():
- 一次性查全字典表,用
Set<string></string>去重,再遍历调用str.intern() - 将结果存入
ConcurrentHashMap<string string></string>,key 是原始值,value 是 intern 后的唯一引用 - 业务代码中统一通过该 Map 获取标准化引用,不再直接 new 或从 ResultSet 取原始字符串
注意:只对确定有限的编码类字段做,跳过 remark、title、desc 等不可控字段;Java 7+ 后常量池在堆内,不会引发 PermGen 问题,但滥用仍会撑爆堆。
用枚举或 ID 映射替代字符串字典对象
真正治本的方式是重构建模:
- 将高频字典字段转为
enum,JVM 天然单例,无重复对象,无 GC 压力 - 或在数据库层用整型 ID(如
status_id TINYINT),Java 层用Map<integer statusenum></integer>缓存映射关系 - 这样每条记录只占几个字节(int + 引用),而非几十~上百字节的字符串对象
比起在堆里堆百万个“启用”“禁用”字符串,用一个 enum 实例 + 百万次引用,内存节省通常达 90% 以上。
强引用缓存必须配清理机制和容量边界
如果业务强制要求“强引用+动态更新”,就不能无限制堆积:
- 用
ConcurrentHashMap替代static List,避免全局锁和扩容抖动 - 设置最大缓存条数(如 5000),超限时按 LRU 或写时间淘汰旧项
- 提供显式刷新入口(如 HTTP 管理端点),字典变更时清空对应缓存,而不是等下次 full GC
- 禁止在分页查询、流式处理等场景中边查边 put 到强引用缓存里
没有失效机制的强引用缓存,本质上是把 GC 的责任转嫁给了人肉运维。










