integer等包装类因被静态集合强引用而引发内存泄漏,数量累积导致老年代持续增长且难监控;需用jstat和mat定位“数量多、retained heap大、路径通向static field”的小对象;修复须避免clear()不彻底或weakhashmap仅key弱引用等误区,推荐caffeine或expiringmap并压测验证。

这类泄漏不靠大对象撑满堆,而是靠无数个看似无害的 Integer、Boolean、Long 等包装类,在静态集合中长期堆积——它们本身小,但数量级上去后,叠加 GC Root 的强持有,会持续推高老年代占用,且不易被常规监控捕获。
为什么基本类型包装类也会泄漏
包装类(如 Integer)不是原始类型,是实实在在的堆对象;即使值在缓存范围内(如 -128~127 的 Integer 有缓存),只要被静态集合持有着,就属于强引用链末端。JVM 不会因为“它很小”或“它可复用”就提前回收——只要集合不清理,它就永远活。
- 静态
List<integer></integer>每次add(42),都可能新增一个新对象(超出缓存范围时一定新建) -
static Map<string boolean></string>存状态标记,key 不删、value 不置空 → 所有Boolean.TRUE/FALSE实例全被钉住 - 日志埋点、指标计数器、灰度开关状态等场景,极易出现“只增不减”的静态集合
用 jstat + MAT 锁定真实元凶
别只盯着大对象;重点看“数量异常多、且路径通向 static field”的小对象:
- 先运行
jstat -gc <pid></pid>,观察OU(老年代使用量)是否缓慢但稳定上涨,且多次FGC后不回落 - 导出堆快照:
jmap -dump:format=b,file=heap.hprof <pid></pid> - 在 MAT 中打开 → Dominator Tree → 按
java.lang.Integer或java.lang.Boolean排序 → 查看“Shallow Heap”不大但“Retained Heap”总和巨大、且“Number of Objects”达数万/百万级的类 - 对任一典型实例右键 → Path to GC Roots → exclude all except strong refs → 若终点为
java.lang.Class → static field → xxx.xxx.CacheHolder.list,即确认
修复不能只靠 .clear() 或换 WeakHashMap
包装类泄漏常因修复方式不当而延续:
-
list.clear()后若没同步清空引用(比如还有其他 static 变量指向该 list),下次 add 仍继续涨 -
WeakHashMap<string integer></string>:key 弱引用有效,但 value(Integer)仍是强引用,照样阻止回收 - 用
ConcurrentHashMap但没配过期策略?写入不停、扫描不触发 → 还是涨 - 真正有效的组合:用
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES)替代手写静态集合;或改用带 TTL 的ExpiringMap
上线前必须验证的两个细节
修复后不等于结束,得证明它真稳了:
- 压测时开启
-XX:+PrintGCDetails -Xlog:gc*:file=gc.log,连续跑 45 分钟以上,确认OldGen used曲线平稳、无阶梯式上升 - 用 MAT 对比修复前后两次 dump:相同路径下
Integer实例数应下降 90% 以上,且Path to GC Roots不再指向你的静态类字段










