Java项目上线后出现内存泄露,踩坑历程回顾!

张开发
2026/4/21 19:36:12 15 分钟阅读

分享文章

Java项目上线后出现内存泄露,踩坑历程回顾!
此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满。协助开发人员去分析定位发现内存中某个Object的量远远超出了预期的范围很明显出现内存泄漏了。结合代码分析发现泄漏的这个对象主要存在一个全局HashMap中是作为HashMap的Key值。第一反应就是这里key对应类没有去覆写equals()和hashCode()方法但对照代码仔细一看却发现其实已经按要求提供了自定义的equals和hashCode方法了。进一步走读业务实现逻辑才发现了其中的玄机。踩坑历程回顾鉴于项目代码相对保密这里举个简单的DEMO来辅助说明下。场景内存中构建一个HashMapUser, ListPost映射集用于存储每个用户最近的发帖信息(只是个例子实际工作中如果遇到这种用户发帖缓存的场景一般都是用的集中缓存而不是单机缓存)。用户信息User类定义如下Data public class User { // 用户名称 private String userName; // 账号ID private String accountId; // 用户上次登录时间每次登录的时候会自动更新DB对应时间 private long lastLoginTime; Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return likedCount user.likedCount Objects.equals(userName, user.userName) Objects.equals(accountId, user.accountId); } Override public int hashCode() { return Objects.hash(userName, accountId, likedCount); } }实际使用的时候,用户发帖之后会将这个帖子信息添加到用户对应的缓存中。/** * 将发帖信息加入到用户缓存中 * * param currentUser 当前用户 * param postContent 帖子信息 */ public void addCache(User currentUser, Post postContent) { cache.computeIfAbsent(currentUser, k - new ArrayList()).add(postContent); }当实际运行的时候会发现问题就来了Map中的记录越来越多远超系统内实际的用户数量。为什么呢仔细看下User类就可以知道了原来编码的时候直接用IDE工具自动生成的equals和hashCode方法里面将lastLoginTime也纳入计算逻辑了。这样每次用户重新登录之后对应hashCode值也就变了这样发帖的时候判断用户是不存在Map中的就会再往map中插入一条随着时间的推移内存中数据就会越来越多导致内存泄漏。这么一看其实问题很简单。但是实际编码的时候很多人往往又会忽略这些细节、或者当时可能没有这个场景后面维护的人新增了点逻辑就会出问题 —— 说白了就是埋了个坑给后面的人踩上了。hashCode覆写的讲究hashCode即一个Object的散列码。HashCode的作用对于List、数组等集合而言HashCode用途不大对于HashMap\HashTable\HashSet等集合而言HashCode有很重要的价值。HashCode在上述HashMap等容器中主要是用于寻域即寻找某个对象在集合中的区域位置用于提升查询效率。一个Object对象往往会存在多个属性字段而选择什么属性来计算hashCode值具有一定的考验如果选择的字段太多而HashCode()在程序执行中调用的非常频繁势必会影响计算性能如果选择的太少计算出来的HashCode势必很容易就会出现重复了。为什么hashCode和equals要同时覆写这就与HashMap的底层实现逻辑有关系了。对于JDK1.8版本中HashMap底层的数据结构形如下图所示使用数组链表或者红黑树的结构形式给定key进行查询的时候分为2步调用key对象的hashCode()方法获取hashCode值然后换算为对应数组的下标找到对应下标位置根据hashCode找到的数组下标可能会同时对应多个key所谓的hash碰撞不同元素产生了相同的hashCode值这个时候使用key对象提供的equals()方法进行逐个元素比对直到找到相同的元素返回其所对应的值。根据上面的介绍可以概括为hashCode负责大概定位先定位到对应片区equals负责在定位的片区内精确找到预期的那一个这里也就明白了为什么hashCode()和equals()需要同时覆写。数据退出机制的兜底其实说到这里全局Map出现内存泄漏还有一点就是编码实现的时候缺少对数据退出机制的考虑。参考下redis之类的依赖内存的缓存中间件都有一个绕不开的兜底策略即数据淘汰机制。对于业务类编码实现的时候如果使用Map等容器类来实现全局缓存的时候应该要结合实际部署情况确定内存中允许的最大数据条数并提供超出指定容量时的处理策略。比如我们可以基于LinkedHashMap来定制一个基于LRU策略的缓存Map来保证内存数据量不会无限制增长这样即使代码出问题也只是这一个功能点出问题不至于让整个进程宕机。public class FixedLengthLinkedHashMapK, V extends LinkedHashMapK, V { private static final long serialVersionUID 1287190405215174569L; private int maxEntries; public FixedLengthLinkedHashMap(int maxEntries, boolean accessOrder) { super(16, 0.75f, accessOrder); this.maxEntries maxEntries; } /** * 自定义数据淘汰触发条件在每次put操作的时候会调用此方法来判断下 */ protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() maxEntries; } }总结梳理下几个要点最好不要使用Object作为HashMap的Key如果不得已必须要使用除了要覆写equals和hashCode方法覆写的equals和hashCode方法中一定不能有频繁易变更的字段内存缓存使用的Map最好对Map的数据记录条数做一个强制约束提供下数据淘汰策略。好啦关于这个问题的分享就到这里咯你是否有在工作中遇到此类相同或者相似的问题呢欢迎一起分享讨论下哦~

更多文章