Java中的HashMap内存泄漏问题详解
作者:唐宋xy
前言
众所周知, 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内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!