Java中的HashMap内存泄漏问题详解

 更新时间:2023年09月07日 10:19:34   作者:唐宋xy  
这篇文章主要介绍了Java中的HashMap内存泄漏问题详解,WeakHashMap中的key是弱引用,如果再使用之后没有及时remove掉这个key,那么当GC时key就可能会被回收,导致key对应的value对象占用的内存无法回收进而导致内存泄漏,需要的朋友可以参考下

前言

众所周知, WeakHashMap 中的 key 是弱引用,如果再使用之后没有及时 remove 掉这个key,那么当GC时key就可能会被回收,导致key对应的value对象占用的内存无法回收进而导致内存泄漏,如果有大量的key可能会导致内存溢出、频繁FullGC等问题。

说了这么多导致内存泄漏是因为 WeakHashMap 的 key 是弱引用从而导致内存泄漏,但是 HashMap 的key是强引用,也会导致内存泄漏吗?

Java中的四种引用

先简单了解一下Java中的四种引用分别是什么

强引用

如果一个对象是强引用, GC 不会回收该对象(在该对象可达时),就算发生内存溢出也不会回收该对象。一般手动 new 出来的对象都是强引用

什么时候回收强引用对象呢?平常写代码中那么多 new 的对象怎么没有发生内存溢出呢

平常代码中有大量的例如:Objedt obj = new Object() 这种手动创建的对象既然都是强引用,不会被回收,JVM还不是分分钟爆炸?

其实,强引用对象也是会被回收的,根据GC回收的算法—可达性分析算法进行判断,当一个对象不可达时就会被回收(不可达就不继续展开啦),或者手动将 obj=null ,也会回收该对象。因为obj指向这个 new 出来的对象,所以是可达的对象不会被回收,但是 obj 一般是在栈中,当前线程执行完成,该方法栈就会被回收,所以obj也会被回收,进而导致堆中该对象没有引用可达而被回收。

软引用

如果堆内存空间充足,则一般GC时不会回收软引用对象,只有在堆内存空间不足的时候才会回收软引用内存空间的对象。使用场景:(和弱引用相同的作用)一般可以用作本地缓存,防止缓存中的数据量太大导致内存溢出

弱引用

无论堆内存空间是否充足,在每次GC的时候都可能会对弱引用对象进行回收。

虚引用

虚引用,正如其名,对一个对象而言,这个引用形同虚设,有和没有一样,简单来说虚引用就相当于没有引用。它的作用就是在GC的时候会发出一个系统通知

HashMap内存泄漏场景

HashMap的 put 方法分析

先分析一下HashMap的put/get

HashMap 的 put 方法在保存数据的时候会根据** key 计算出来hash值**,然后根据hash值获取到数组的下标的位置,没有发生hash冲突就只直接保存,如果发生hash冲突则形成链表。

get 方法是根据key的hash值得到数组的下标,然后直接获取或者遍历链表并调用 equals 方法来获取数据。

第一种:内存泄漏代码

下面的代码会导致内存泄漏吗

class Person {
  String name;
  int age;
  //省略getter/setter方法
  ...
}
HashMap<Person, Integer> map = new HashMap<>();
Person gay = new Person("gay伦", 18);
Person yase = new Person("亚瑟", 10);
Person timo = new Person("提莫", 11);
// 保存
map.put(gay, 1);
map.put(yase, 2);
map.put(timo, 3);
// 第一次获取
Integer x = map.get(gay);
System.out.println(x);
Integer y = map.get(new Person("gay伦", 18));
System.out.println(y); // 这里可以直接获取到对象吗?

控制台输出:

null

原因分析

相信注重细节的小伙伴已经看出来了,为什么第二次没有获取到对应的 value 呢,这是因为 Person 没有重写** hashcode 和 equals ,导致默认的计算的 hashcode 不同,所以无法获取到第一次保存的 value (还记得上面的前戏**吗?前戏是非常重要的)

简单说就是两次计算的 hashcode 和 equals 不同导致HashMap的 get 方法无法获取到对应的 value

如果 HashMap 一直没有回收(例如定义为常量、静态变量),那么无法 remove 对应的key,就会导致value一直被HashMap引用,从而导致占用的内存无法回收,导致内存泄漏。

解决方案

解决方案:如果是自定义的类,最好重写 hashcode 和 equals ,如果是保存到Map中,则一定要重写 hashcode 和 equals

修正代码示例

使用IDEA自动生成的hashcode和equals

...
// 加上equals和hashcode =》 IDEA自动生成
@Override
public boolean equals(Object o) {
  if (this == o) return true;
  if (o == null || getClass() != o.getClass()) return false;
  Person person = (Person) o;
  return age == person.age &&
  Objects.equals(name, person.name);
}
@Override
public int hashCode() {
	return Objects.hash(name, age);
}
...

控制台输出:

11

第二种:内存泄漏代码

看看下面的代码是否会导致内存泄漏

class Person {
  String name;
  int age;
  //省略getter/setter方法
  ...
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
    Objects.equals(name, person.name);
  }
  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }
}
HashMap<Person, Integer> map = new HashMap<>();
Person gay = new Person("gay伦", 18);
Person yase = new Person("亚瑟", 10);
Person timo = new Person("提莫", 11);
// 保存
map.put(gay, 1);
map.put(yase, 2);
map.put(timo, 3);
// 第一次获取
Integer x = map.get(gay);
System.out.println(x);
// 修改对象的name属性
gay.setName("盖伦");
// 第二次获取
Integer y = map.get(gay);
System.out.println(y); // 这里可以获取到对象吗?

控制台输出:

null

原因分析

怎么就改一下 name 就获取不到了?(其实还是要看前戏)看见上面的 hashcode 和 equals 了没有, key 的 hashcode 是通过 Objects.hash(name, age); 计算的,hash值和 name 和 age 有关的,如果其中一个改变了,那么计算出来的 hash 值就不同,计算出来的下标可能就不同,所以就获取不到 value

解决方案

在重写 hashcode 和 equals 方法的时候,一定要注意参与计算的相关字段,最好不要更改参与计算的字段属性值,如果可以的话,可以将参与计算的字段定义为 final 类型

修正代码演示

class Person {
  String name;
  // 定义为final
  final int age;
  //省略getter/setter方法
  ...
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    // 只判断name相同即可
    return age == person.age;
  }
  @Override
  public int hashCode() {
    // hash值通过final定义的age计算
    return Objects.hash(age);
  }
}

控制台输出:

11

小总结

WeakHashMap 在某些情况下因为 key 是弱引用,所以会导致内存溢出。

ThreadLocal 为什么会内存溢出呢?

就是因为其内部类 Entry 继承了 WeakReference ,所以一般使用完成之后要及时的 remove HashMap在某些情况下也会内存溢出,所以在自定义类的时候要注意重写equals和hashcode方法。

到此这篇关于Java中的HashMap内存泄漏问题详解的文章就介绍到这了,更多相关HashMap内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java使用Random类生成随机数示例

    Java使用Random类生成随机数示例

    这篇文章主要介绍了Java使用Random类生成随机数,结合实例形式分析了java基于Random类生成随机数与遍历输出相关操作技巧,需要的朋友可以参考下
    2019-07-07
  • Java之stream流求字段累计和的3种方法总结

    Java之stream流求字段累计和的3种方法总结

    Java中Stream流用来帮助处理集合,类似于数据库中的操作,下面这篇文章主要介绍了Java之stream流求字段累计和的3种方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-09-09
  • 关于Java下奇怪的Base64详解

    关于Java下奇怪的Base64详解

    这篇文章主要给大家介绍了关于Java下奇怪的Base64的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Mybatis返回int或者Integer类型报错的解决办法

    Mybatis返回int或者Integer类型报错的解决办法

    这篇文章主要介绍了Mybatis返回int或者Integer类型报错的解决办法,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-12-12
  • Java集合排序规则接口Comparator用法解析

    Java集合排序规则接口Comparator用法解析

    这篇文章主要介绍了Java集合排序规则接口Comparator用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • logback-spring.xml配置解析(最新整理)

    logback-spring.xml配置解析(最新整理)

    logback-spring.xml用于配置Spring Boot项目日志,定义输出格式、路径、级别,支持动态参数读取(如SpringProperty),实现日志隔离与滚动策略(按天+大小),并通过logger继承机制控制日志输出目的地,对logback-spring.xml配置相关知识感兴趣的朋友一起看看吧
    2025-07-07
  • Java发展史之Java由来

    Java发展史之Java由来

    本文主要给大家简单讲解了一下java的发展史,详细说明了java的由来以及如何一步步发展起来的,想了解的小伙伴可以来参考下
    2016-10-10
  • Java拆箱与装箱实例详解

    Java拆箱与装箱实例详解

    这篇文章主要介绍了Java拆箱与装箱,结合实例形式详细分析了Java拆箱与装箱相关的数据类型转换操作技巧,需要的朋友可以参考下
    2019-11-11
  • SpringBoot整合weixin-java-pay实现微信小程序支付的示例代码

    SpringBoot整合weixin-java-pay实现微信小程序支付的示例代码

    微信小程序支付是常见的一种功能,本文主要介绍了SpringBoot整合weixin-java-pay实现微信小程序支付的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2024-05-05
  • java环境变量如何配置

    java环境变量如何配置

    这篇文章主要为大家详细介绍了java环境变量配置教程,感兴趣的小伙伴们可以参考一下
    2016-06-06

最新评论