为什么不建议使用Java自定义Object作为HashMap的key
作者:??架构悟道???? 发布时间:2021-09-21 06:15:05
前言
此前部门内的一个线上系统上线后内存一路飙高、一段时间后直接占满。协助开发人员去分析定位,发现内存中某个Object的量远远超出了预期的范围,很明显出现内存泄漏了。
结合代码分析发现,泄漏的这个对象,主要存在一个全局HashMap中,是作为HashMap的Key值。第一反应就是这里key对应类没有去覆写equals()和hashCode()方法,但对照代码仔细一看却发现其实已经按要求提供了自定义的equals和hashCode方法了。进一步走读业务实现逻辑,才发现了其中的玄机。
踩坑历程回顾
鉴于项目代码相对保密,这里举个简单的DEMO来辅助说明下。
场景: 内存中构建一个HashMap<User, List<Post>>
映射集,用于存储每个用户最近的发帖信息(只是个例子,实际工作中如果遇到这种用户发帖缓存的场景,一般都是用的集中缓存,而不是单机缓存)。
用户信息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 lastLoginTime == user.lastLoginTime &&
Objects.equals(userName, user.userName) &&
Objects.equals(accountId, user.accountId);
}
@Override
public int hashCode() {
return Objects.hash(userName, accountId, lastLoginTime);
}
}
实际使用的时候,用户发帖之后,会将这个帖子信息添加到用户对应的缓存中。
/**
* 将发帖信息加入到用户缓存中
*
* @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 FixedLengthLinkedHashMap<K, V> extends LinkedHashMap<K, 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.Entry<K, V> eldest) {
return size() > maxEntries;
}
}
最好不要使用Object作为HashMap的Key
如果不得已必须要使用,除了要覆写equals和hashCode方法
覆写的equals和hashCode方法中一定不能有频繁易变更的字段
内存缓存使用的Map,最好对Map的数据记录条数做一个强制约束,提供下数据淘汰策略。
来源:https://juejin.cn/post/7114555022599258119


猜你喜欢
- 闲来无事,做了一个简单的抽奖转盘的ui实现,供大家参考package com.microchange.lucky; import andro
- 项目中需要实现一个状态显示的悬浮框,要求可以设置两种模式:拖动模式和不可拖动模式。实现效果图如下:实现步骤:1.首先要设置该悬浮框的基本属性
- 1. 问题所示编译ssm的项目的时候出现了这个错误导致一直运行不起来SLF4J: Failed to load class "or
- maven配置项目的jdk版本无效排查最近在配置项目的jdk的时候发现在pom.xml中配置的1.8版本无效,maven更新后就变成了1.7
- 首先我们常用的注解包括@Entity、@Table、@Id、@IdClass、@GeneratedValue、@Basic、@Transie
- 是否允许循环依赖和bean的命名重复取决于beanfactory的两大属性allowBeanDefinitionOverriding和all
- 在Spring4之后,要使用注解开发,必须要保证aop的包导入了使用注解需要导入context约束,增加注解的支持!<?xml ver
- 本文实例为大家分享了六种Android常见控件的使用方法,供大家参考,具体内容如下1、TextView 主要用于界面上显示一段文本
- 本文实例为大家分享了java实现人机猜拳游戏的具体代码,供大家参考,具体内容如下完成人机猜拳互动游戏的开发,用户通过控制台输入实现出拳,电脑
- 迪杰斯特拉算法迪杰斯特拉算法是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算
- 先看下面的这组字符,如果输出来,它是无法靠右对齐: Source Codestring[] s1 = { "300",
- 学习时,接触使用到IDEA这个开发工具。在用IDEA开发的时候,需要创建工程。以下介绍各类型项目的新建。一、 springboot工程简介:
- Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new
- Step 1.依赖bannerGradledependencies{ compile 'com.youth.banner
- 这篇文章主要介绍了Spring事务失效问题分析及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的
- 引言异步蓝图节点:在蓝图节点的右上角有时钟图标。注意:异步节点可以在EventGraph/Macros中使用,但是无法在蓝图函数中使用。AI
- 前言平时的工作中经常碰到很多疑难问题的处理,在解决问题的同时,有一些工具起到了相当大的作用,在此书写下来,一是作为笔记,可以让自己后续忘记了
- 本文为大家分享了Android AIDL实现两个APP间的跨进程通信实例,供大家参考,具体内容如下1 Service端创建首先需要创建一个A
- 实例如下:XSSFilter.javapublic void doFilter(ServletRequest servletrequest,
- 本文实例讲述了C#计算字符串相似性的方法。分享给大家供大家参考。具体如下:计算字符串相似性的办法很多,甚至最笨的办法可以挨个匹配,这里要讲的