引言

今天在排查Tomcat服务崩溃异常的时候,碰到了关于HashSet的底层实现的问题。所以就借着这个机会把HashSet的源码进行下分析,以表示下有些源码不看不知道,一看吓一跳的内心变化。

  • 起因
  • HashSet源码分析
  • 总结

一、起因

系统上线前夕,在进行测试的时候,测试小姐姐突然找过来说性能测试的环境Tomcat崩溃了,平台已经无法正常访问了。纳尼,平台崩溃,听到这个信息,我内心是无比绝望的。
JDK源码分析系列之四:HashSet深入理解以及源码分析_Java集合
绝望归绝望,问题还是要分析的。首先我们需要分析下问题产生的原因到底是什么。在Tomcat的目录下,发现了hronf文件,即Tomcat的内存镜像文件。我们需要对其进行分析,找到内存发生溢出的地方。通过使用工具我们发现此处的HashSet中的存在了十几万的对象,占据了大部分的内存空间。如下图所示,而Tomcat的使用空间是定额的,当此处的set中的对象达到了十三万多个。这就是Tomcat内存溢出的根本原因。经过询问,测试为了进行性能测试,造了很多的假数据,导致某项检测功能中一下子放入了过多的检测对象,而这些检测对象无法满足消除的条件,所以在此缓存set集合中越积越多,最终导致内存溢出问题的产生。
JDK源码分析系列之四:HashSet深入理解以及源码分析_java_02
如上图所示,在分析hronf文件时发现HashSet中包含了HashMap对象,看到这里 惊呆了,HashSet怎么会包含HashMap对象呢?是不是搞错了。于是决定好好看下HashSet的源码中底层实现到底是怎样的。

二、HashSet源码分析

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

...
}

从以上源码我们可以看出:
a、HashSet的底层实现是通过HashMap进行数据存储的;
b、PRESENT 对应的是new Object(),它是HashMapvalue值;

1、构造函数
如下源码是HashSet的四个构造函数:

public HashSet() {
        map = new HashMap<>();
    }
    
public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
  
public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

2、元素查找
HashSet源码中提供了contains(Object o)查看是否包含指定元素的方法,实际上其底层调用的是HashMap.containsKey(Object key)判断是否包含指定key。自此我们了解到HashSet实际上是使用HashMapkey值来进行数据的存储的,这也解释了HashSet的值不可以重复的根本原因。

 public boolean contains(Object o) {
        return map.containsKey(o);
    }

3、元素添加
HashSet中提供了add(E e)添加元素的方法,实际底层调用的是HashMap中的put(K key, V value)方法,首先判断元素(也就是key)是否存在,如果不存在则插入,如果存在则不插入,这样HashSet中就不存在重复值。

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

4、元素删除与清空
这里就不多做介绍了。

public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

public void clear() {
        map.clear();
    }

三、总结

其实HashSet中的源码还是非常简单的,底层实现都是通过HashMap来进行的,并且通过HashMapkey来进行元素的存储。以前我们都是关注HashMap的源码实现,对于HashSet的源码没有在意,如果不是这次分析崩溃的问题也不会发现HashSet的底层实现逻辑。所以有时候越简单的懂越要知道其内部实现,这样就不会阴沟里翻船了。