不同Java映射类型的每个映射值的相对内存开销是多少

时间:2014-10-07 21:23:48

标签: java performance memory

我有一个案例,我有一个“属性映射”,每个属性映射一个非常大的对象集合。我只对地图本身的开销感兴趣,而不是附加的键和值。

许多人拥有少量属性,即:< 5,很多都没有。对于空映射,我使用单例“空实例”作为优化。

对于速度,似乎在< = 5的计数时,TreeMap似乎等于或优于HashMap用于检索,并且当移除节点时“自动修剪”。我认为HashMap会为每个条目使用散列/句柄数组的内存开销,包括满载空条目句柄的加载因子之上的内存,以及密钥的对象开销和句柄存储以及“Entry [+ internal”中的值字段,如果有的话)“。

另外我认为TreeMap只使用每个Entry Object开销+ key,value,prev,列表条目中的下一个句柄字段,以及尽可能小的数组二进制搜索 - 一个数组对象(修剪为size)只有键和值的句柄。

有没有人真正检查过每种Java地图类型的准确完整开销?对于我的具有属性的bazillions对象,它会在1到5的属性情况下产生显着差异,而不会影响访问速度。

4 个答案:

答案 0 :(得分:3)

https://code.google.com/p/memory-measurer/wiki/ElementCostInDataStructures

TreeMapHashMap每个条目使用大约相同的内存量,但TreeMap对于地图本身的常量开销略小,这是正确的。

答案 1 :(得分:2)

如果您的属性映射没有主动更新,但是构建一次并且查询的时间长或多,我建议使用特定于Java的内存保存架构:

public interface DynamicMap<K, V> {
    DynamicMap<K, V> add(K key, V value);
    V get(K key);
}

class WrappingDynamicMap<K, V> implements DynamicMap<K, V> {
    DynamicMap<K, V> delegate;
    public void put(K key, V value) { delegate = add(key, value); }

    @Override public DynamicMap<K, V> add(K key, V value) {
        if (delegate == null) return new Map1<>(key, value);
        return delegate.add(key, value);
    }

    @Override public V get(K key) { return delegate.get(key); }
}

class Map1<K, V> implements DynamicMap<K, V> {
    K k1; V v1;
    Map1(K k1, V v1) { this.k1 = k1; this.v1 = v1; }

    boolean putThis(K key, V value) {
        if (key.equals(k1)) { v1 = value; return true; }
        return false;
    }

    @Override public DynamicMap<K, V> add(K key, V value) {
        if (putThis(key, value)) return this;
        return new Map2<>(this, key, value);
    }

    @Override public V get(K key) { return key.equals(k1) ? v1 : null; }
}

class Map2<K, V> extends Map1<K, V> {
    K k2; V v2;
    Map2(Map1<K, V> m, K k2, V v2) {
        super(m.k1, m.v1); this.k2 = k2; this.v2 = v2;
    }

    @Override boolean putThis(K key, V value) {
        if (super.putThis(key, value)) return true;
        if (key.equals(k2)) { v2 = value; return true; }
        return false;
    }

    @Override public DynamicMap<K, V> add(K key, V value) {
        if (putThis(key, value)) return this;
        HashMap<K,V>hm=new HashMap<K,V>(){{
            put(k1,v1);put(k2,v2);put(key,value);}};
        return new BigMap<>(hm);
    }

    @Override public V get(K key) {
        V v = super.get(key);
        return v != null ? v : (key.equals(k2) ? v2 : null);
    }
}

// Map3, Map4, Map5

class BigMap<K, V> implements DynamicMap<K, V> {
    private HashMap<K, V> impl;
    BigMap(HashMap<K, V> impl) { this.impl = impl; }

    @Override public DynamicMap<K, V> add(K key, V value) {
        impl.put(key, value); return this;
    }

    @Override public V get(K key) { return impl.get(key); }
}

如果您需要经典地图界面,请将WrappingDynamicMapput()一起使用,如果您能够在每个地图上的对象内重新分配属性地图字段,则可以直接使用DynamicMap实施使用add(),每个查询都可以安全地取消引用1个,并且每个属性映射在堆上有16-24个字节。

这种方法的优点:

  • 我敢打赌,这会比HashMap / TreeMap在尺寸1..3-5上快得多,具体取决于您的密钥的equals()复杂性
  • 使用绝对最小内存来保存数据
  • 可以轻松专门用于原始键/值

缺点:

  • 地图上的GC压力放置/删除

我希望很清楚如何为Map1 / Map2 /../ BigMap实施删除。 BigMap还可以监控impl.size()并最终转回Map5

其他机会:

  • 对于5到12之间的大小,可以使用非常简单的开放寻址哈希表实现(使用常数模16)代替HashMap,大小存储为byte

答案 2 :(得分:2)

假设您想要真正优化并需要完整的Map,您可以将Object[]封装在自己的AbstractMap中,并将所有键和值交错放置。您需要实现一个使用AbstractSet的方法,但随后所有变异方法默认抛出它会变得更糟。因此,您必须全部检查......可能需要一整天。

通过番石榴测试,您可以确保您的地图真正有用。如果没有真正深刻的测试,我建议坚持使用自己的小界面,因为Map非常庞大。

答案 3 :(得分:0)

好的 - 我查看了Java源代码,是的,TreeMap确实为每个节点使用了相当数量的内存。所以我做了两个地图实现并测试了它们。

一个简单的单链表,父表只有两个字段MyMap {Entry root; int size;条目{key,value,next}大小&lt; = 8的性能比哈希映射更快,易于编码,更小的大小线性更快。

我还使用二进制搜索执行了交错的键/值数组,这比单纯的列表更糟糕,因为所有调用Comparable.compare()方法的额外开销都会在键上获得所有增益并且它的执行速度不会超过HashMap的尺寸较小,随着它变大而变得更糟。

因此,就我而言,最简单的事情就是获胜。当它大于8时,我只需用哈希映射替换它,如果HashMap的大小低于5,我将恢复到列表映射。

你是正确的。对于这个应用程序,可能只有一个put可以获得几百个,并且大多数put只是替换条目中的值而不会丢弃它。