【问题标题】:Memory overhead of Java HashMap compared to ArrayListJava HashMap 与 ArrayList 相比的内存开销
【发布时间】:2010-12-04 08:07:27
【问题描述】:

我想知道 java HashMap 与 ArrayList 相比的内存开销是多少?

更新:

我想提高搜索一大包(6 百万以上)相同对象的特定值的速度。

因此,我正在考虑使用一个或多个 HashMap 而不是使用 ArrayList。但我想知道 HashMap 的开销是多少。

据我了解,key没有被存储,只有key的hash,所以应该是对象的hash大小+一个指针

但是使用什么哈希函数呢?是the one offered by Object 还是另一个?

【问题讨论】:

  • 根本不重复,因为我问的是与 ArrayList 相比,HashMap 将使用多少内存。
  • 您是否考虑使用两个 ArrayList 与一个 HashMap?
  • 您只存储哈希是错误的。存储整个密钥。

标签: java arraylist hashmap memory-management


【解决方案1】:

如果您将 HashMap 与 ArrayList 进行比较,我认为您正在对 ArrayList 进行某种搜索/索引,例如二进制搜索或自定义哈希表...?因为 .get(key) 到 600 万个条目使用线性搜索是不可行的。

使用该假设,我进行了一些实证测试并得出结论:“如果您将 ArrayList 与二进制搜索或自定义哈希映射实现一起使用,您可以在相同数量的 RAM 中存储 2.5 倍的小对象,与 HashMap 相比”。我的测试是基于只包含3个字段的小对象,其中一个是key,key是一个整数。我使用的是 32 位 jdk 1.6。有关“2.5”图的注意事项,请参见下文。

需要注意的关键点是:

(a) 杀死你的不是引用或“负载因子”所需的空间,而是对象创建所需的开销。如果键是原始类型,或者是 2 个或更多原始值或引用值的组合,则每个键都需要自己的对象,这会带来 8 个字节的开销。

(b) 根据我的经验,您通常需要将键作为值的一部分(例如,为了存储由客户 ID 索引的客户记录,您仍然希望客户 ID 作为客户对象的一部分)。这意味着 HashMap 单独存储对键和值的引用有点浪费。

注意事项:

  1. HashMap 键最常用的类型是字符串。对象创建开销不适用于此处,因此差异会更小。

  2. 我得到一个 2.8 的数字,即 8880502 个条目插入到 ArrayList 中,而 3148004 个条目插入到 -Xmx256M JVM 上的 HashMap,但我的 ArrayList 负载因子为 80%,我的对象非常小 - 12 个字节加 8字节对象开销。

  3. 我的图和我的实现要求键包含在值中,否则我会在对象创建开销方面遇到同样的问题,这只是 HashMap 的另一种实现。

我的代码:

public class Payload {
    int key,b,c;
    Payload(int _key) { key = _key; }
}


import org.junit.Test;

import java.util.HashMap;
import java.util.Map;


public class Overhead {
    @Test
    public void useHashMap()
    {
        int i=0;
        try {
            Map<Integer, Payload> map = new HashMap<Integer, Payload>();
            for (i=0; i < 4000000; i++) {
                int key = (int)(Math.random() * Integer.MAX_VALUE);
                map.put(key, new Payload(key));
            }
        }
        catch (OutOfMemoryError e) {
            System.out.println("Got up to: " + i);
        }
    }

    @Test
    public void useArrayList()
    {
        int i=0;
        try {
            ArrayListMap map = new ArrayListMap();
            for (i=0; i < 9000000; i++) {
                int key = (int)(Math.random() * Integer.MAX_VALUE);
                map.put(key, new Payload(key));
            }
        }
        catch (OutOfMemoryError e) {
            System.out.println("Got up to: " + i);
        }
    }
}


import java.util.ArrayList;


public class ArrayListMap {
    private ArrayList<Payload> map = new ArrayList<Payload>();
    private int[] primes = new int[128];

    static boolean isPrime(int n)
    {
        for (int i=(int)Math.sqrt(n); i >= 2; i--) {
            if (n % i == 0)
                return false;
        }
        return true;
    }

    ArrayListMap()
    {
        for (int i=0; i < 11000000; i++)    // this is clumsy, I admit
            map.add(null);
        int n=31;
        for (int i=0; i < 128; i++) {
            while (! isPrime(n))
                n+=2;
            primes[i] = n;
            n += 2;
        }
        System.out.println("Capacity = " + map.size());
    }

    public void put(int key, Payload value)
    {
        int hash = key % map.size();
        int hash2 = primes[key % primes.length];
        if (hash < 0)
            hash += map.size();
        do {
            if (map.get(hash) == null) {
                map.set(hash, value);
                return;
            }
            hash += hash2;
            if (hash >= map.size())
                hash -= map.size();
        } while (true);
    }

    public Payload get(int key)
    {
        int hash = key % map.size();
        int hash2 = primes[key % primes.length];
        if (hash < 0)
            hash += map.size();
        do {
            Payload payload = map.get(hash);
            if (payload == null)
                return null;
            if (payload.key == key)
                return payload;
            hash += hash2;
            if (hash >= map.size())
                hash -= map.size();
        } while (true);
    }
}

【讨论】:

  • 嗨蒂姆。在大多数情况下,密钥集是明确定义且有限的。我相信您可以通过添加键缓存来驱逐键的对象创建来极大地优化您的代码。你怎么看?
  • @Rafael Sanches :您能解释一下“添加密钥缓存以驱逐对象创建”是什么意思吗?
【解决方案2】:

最简单的方法是查看源代码并以这种方式解决问题。但是,您实际上是在比较苹果和橙子 - 列表和地图在概念上是截然不同的。您很少会根据内存使用情况在它们之间进行选择。

这个问题的背景是什么?

【讨论】:

  • 我总是对这些 ArrayList 与 HashMap 的问题感到惊讶。 ArrayList vs HashSet 我觉得很有意义,但 Map 甚至不是 Collection。
  • 这个特殊的问题有点令人困惑,因为它谈论的是 Map 和 List 之间的内存消耗......但是问题可能源于 elhoim 使用非常大的列表并且查找不令人满意的事实(您可以使用 LinkedHashMaps 或多或少地保留顺序)。他们可能不希望他们的应用程序的足迹仅仅因为他们切换到地图而膨胀。
  • Map 严格来说是一个集合(但它不是一个集合):java.sun.com/javase/6/docs/technotes/guides/collections/…
  • TofuBeer:注意我使用的大小写。
  • 我不确定我是否同意这里 - 我偶尔会想“我应该使用 Map 而不是 List”,如果键是稀疏的,否则会有列表中有很多空值,或者我需要以不可预测的顺序填充列表。
【解决方案3】:

其中存储的所有内容都是指针。根据您的架构,指针应该是 32 位或 64 位(或多或少)

10 个数组列表倾向于至少分配 10 个“指针”(以及一些一次性开销)。

映射必须分配两倍(20 个指针),因为它一次存储两个值。然后最重要的是,它必须存储“哈希”。它应该比地图大,在 75% 的负载下它应该是大约 13 个 32 位值(散列)。

所以如果你想要一个临时的答案,这个比率应该是大约 1:3.25 左右,但你只是在谈论指针存储——除非你存储大量对象,否则非常小——如果是这样,实用程序能够立即引用(HashMap)与迭代(数组)应该比内存大小更重要。

哦,还有: 数组可以适合您的集合的确切大小。如果您指定大小,HashMaps 也可以,但如果它“增长”超过该大小,它将重新分配一个更大的数组并且不使用其中的一些,所以那里也可能会有一点浪费。

【讨论】:

  • “一个映射必须分配两倍(20 个指针),因为它一次存储两个值”假设键和值是不同的。我们真的不知道作者希望存储什么,因为他没有给我们很多细节。
  • 映射总是必须为键和值分配存储空间,即使它们是相同的(因此使用了“映射”这个词)。一个 HashSet 可能是您对@matt b 的想法,它只分配一个数组,然后您在对象内部进行映射,在这种情况下,比率约为 1:2.25。
【解决方案4】:

我也没有给你答案,但是快速的谷歌搜索发现了一个 Java 中的函数,它可能会有所帮助。

Runtime.getRuntime().freeMemory();

所以我建议您使用相同的数据填充 HashMap 和 ArrayList。记录空闲内存,删除第一个对象,记录内存,删除第二个对象,记录内存,计算差异,...,利润!!!

您可能应该对大量数据执行此操作。即从 1000 开始,然后是 10000、100000、1000000。

编辑:已更正,感谢 amischiefr。

编辑: 很抱歉编辑您的帖子,但如果您要使用它,这非常重要(而且评论有点多) . freeMemory 不像你想象的那样工作。首先,它的值被垃圾回收所改变。其次,当java分配更多内存时,它的值会发生变化。仅使用 freeMemory 调用并不能提供有用的数据。

试试这个:

public static void displayMemory() {
    Runtime r=Runtime.getRuntime();
    r.gc();
    r.gc(); // YES, you NEED 2!
    System.out.println("Memory Used="+(r.totalMemory()-r.freeMemory()));
}

或者您可以返回使用的内存并将其存储,然后将其与以后的值进行比较。无论哪种方式,请记住 2 个 gcs 并从 totalMemory() 中减去。

再次抱歉,编辑您的帖子!

【讨论】:

  • 方法:“返回Java虚拟机的总内存量。”,不是当前应用程序使用的内存量,也不是剩余内存。为此,您需要调用 freeMemory()
  • @Bill:为避免 gc 更改您的指标,您需要以相同的初始/最大大小启动 VM。如果您引用了数据结构(即数据结构不是 gc'able ),则调用 gc()x2 无效
  • @Oscar,gc() 确实有所不同,因为当集合超出原始数组时,ArrayList 和 HashMap 有时都必须重新分配数组。并且旧数组可能不会立即被释放。
  • @BILL:无需道歉。我只是对JVM有一个基本的了解。感谢您充实细节。如果你还在,你愿意解释为什么需要两次 gc() 调用吗?这是记录在案还是 JVM 怪癖?
  • 呃,当你调用 gc() 时,不能保证 VM 会真正进行完整的垃圾回收。它是非确定性的。连续调用两次只会使机会翻倍。这对我来说似乎很愚蠢。
【解决方案5】:

Hashmap 试图保持一个负载因子(通常是 75% 满),您可以将 hashmap 视为一个稀疏填充的数组列表。直接比较大小的问题是地图的这个负载因子会增长以满足数据的大小。另一方面,ArrayList 通过将其内部数组大小加倍来满足其需求。对于相对较小的大小,它们具有可比性,但是随着您将越来越多的数据打包到映射中,它需要大量空引用才能保持哈希性能。

无论哪种情况,我都建议您在开始添加之前准备好预期的数据大小。这将为实现提供更好的初始设置,并且在两种情况下都可能消耗更少。

更新:

根据您更新的问题查看Glazed lists。这是一些 Google 人员编写的一个简洁的小工具,用于执行与您描述的类似的操作。它也非常快。允许聚类、过滤、搜索等。

【讨论】:

    【解决方案6】:

    HashMap保存对值的引用和对键的引用。

    ArrayList 只是保存对值的引用。

    所以,假设 key 使用与 value 相同的内存,HashMap 会多使用 50% 的内存(虽然严格来说,并不是 HashMap 使用那块内存,因为它只是保留了对它的引用)

    另一方面,HashMap 为基本操作(get 和 put)提供恒定时间性能因此,虽然它可能会使用更多内存,但使用 HashMap 获取元素可能比使用数组列表。

    所以,接下来你应该做的是不要关心谁使用了更多内存,而是他们有什么用处

    为您的程序使用正确的数据结构比在下面实现库的方式节省更多的 CPU/内存。

    编辑

    在格兰特韦尔奇回答之后,我决定测量 2,000,000 个整数。

    这是source code

    这是输出

    $
    $javac MemoryUsage.java  
    Note: MemoryUsage.java uses unchecked or unsafe operations.
    Note: Recompile with -Xlint:unchecked for details.
    $java -Xms128m -Xmx128m MemoryUsage 
    Using ArrayListMemoryUsage@8558d2 size: 0
    Total memory: 133.234.688
    Initial free: 132.718.608
      Final free: 77.965.488
    
    Used: 54.753.120
    Memory Used 41.364.824
    ArrayListMemoryUsage@8558d2 size: 2000000
    $
    $java -Xms128m -Xmx128m MemoryUsage H
    Using HashMapMemoryUsage@8558d2 size: 0
    Total memory: 133.234.688
    Initial free: 124.329.984
      Final free: 4.109.600
    
    Used: 120.220.384
    Memory Used 129.108.608
    HashMapMemoryUsage@8558d2 size: 2000000
    

    【讨论】:

    • 这并不奇怪 - 列表中有 2000000 个元素,但地图中只有 65536 个条目。你为什么要把演员阵容做空? 2000000 也相当大(我得到一个 OutOfMemoryError 与默认堆设置。)最后你省略了格兰特建议的对 System.gc() 的调用。添加这些并将大小减小到 20000 我得到 410,376 字节的列表和 912,680 的地图。
    • 这不是一个很好的测试。您是否意识到,当您打印出堆的大小时,列表或映射可能已经被 GC 了?从而擦除您分配的所有对象。
    • @finnw:我添加了 gc' 调用并使用 double 作为键。结果与您的相似。 HashMap 使用比 ArrayList 更多的内存。 @matt b:它们不是 gc'ed,因为它们是实例变量。我已经修改了代码,所以现在更清楚了(我在最后添加了一个 println 让你看到对象仍然存在)
    • 这是您真正/更好地测量 Java 字节的方法 :) code.google.com/p/memory-measurer
    【解决方案7】:

    基本上,您应该使用“适合工作的工具”。由于在不同的情况下您需要一个键/值对(您可以在其中使用HashMap)和在不同的情况下您只需要一个值列表(您可以在其中使用ArrayList)然后在我看来,“哪个使用更多内存”的问题没有实际意义,因为这不是选择一个而不是另一个的考虑。

    但要回答这个问题,因为 HashMap 存储键/值对,而 ArrayList 只存储值,我认为单独向 HashMap 添加键意味着它会占用更多内存,假设当然,我们是通过相同的值 type 比较它们(例如,两者中的值都是字符串)。

    【讨论】:

      【解决方案8】:

      我认为这里提出了错误的问题。

      如果您想提高在包含 600 万个条目的 List 中搜索对象的速度,那么您应该查看这些数据类型的检索操作执行速度有多快

      像往常一样,这些类的 Javadocs 非常清楚地说明了它们提供的性能类型:

      HashMap:

      此实现为基本操作(get 和 put)提供恒定时间性能,假设哈希函数将元素正确地分散在桶中。

      这意味着 HashMap.get(key) 是O(1)

      ArrayList:

      size、isEmpty、get、set、iterator 和 listIterator 操作在恒定时间内运行。添加操作在摊销常数时间内运行,即添加 n 个元素需要 O(n) 时间。所有其他操作都以线性时间运行(粗略地说)。

      这意味着ArrayList 的大部分操作都是O(1),但可能不是您用来查找与某个值匹配的对象的操作。

      如果您正在迭代 ArrayList 中的每个元素并测试是否相等,或者使用 contains(),那么这意味着您的操作在 O(n) 时间运行(或更糟)。

      如果您不熟悉O(1)O(n) 表示法,这是指操作需要多长时间。在这种情况下,如果你能获得恒定时间的性能,你就想接受它。如果HashMap.get()O(1),这意味着检索操作花费的时间大致相同无论 Map 中有多少条目。

      ArrayList.contains()O(n) 这样的事实意味着它所花费的时间会随着列表大小的增加而增加;因此,通过 ArrayList 迭代 600 万个条目根本不会很有效。

      【讨论】:

      • 对象检索操作很快,因为它们只是 POJO。是的,我知道 HashMap 得到的是 O(1),这就是我想使用它们的原因,但我的问题仍然是 HashMap 将使用多少内存而不是 ArrayList
      • 您的对象是 POJO 的事实与迭代包含它们的列表的速度无关
      【解决方案9】:

      我不知道确切的数字,但 HashMap 更重。比较两者,ArrayList 的内部表示是不言而喻的,但是 HashMaps 保留了 Entry 对象(Entry),这会使你的内存消耗膨胀。

      它没有那么大,但它更大。一个很好的可视化方法是使用动态分析器,例如YourKit,它允许您查看所有堆分配。挺好看的。

      【讨论】:

        【解决方案10】:

        This post 提供了大量有关 Java 中对象大小的信息。

        【讨论】:

          【解决方案11】:

          如果您正在考虑两个 ArrayList 与一个 Hashmap,它是不确定的;两者都是部分完整的数据结构。如果你比较 Vector 和 Hashtable,Vector 可能更节省内存,因为它只分配它使用的空间,而 Hashtables 分配更多空间。

          如果您需要一个键值对并且不做非常耗费内存的工作,只需使用 Hashmap。

          【讨论】:

            【解决方案12】:

            正如 Jon Skeet 所说,这些是完全不同的结构。映射(例如 HashMap)是从一个值到另一个值的映射 - 即您有一个映射到一个值的键,在 Key->Value 类型的关系中。密钥经过哈希处理,并放置在一个数组中以便快速查找。

            另一方面,List 是具有顺序的元素的集合 - ArrayList 恰好使用数组作为后端存储机制,但这无关紧要。每个索引元素都是列表中的单个元素。

            编辑:根据您的评论,我添加了以下信息:

            密钥存储在哈希图中。这是因为不能保证哈希对于任何两个不同的元素都是唯一的。因此,必须在散列冲突的情况下存储密钥。如果您只是想查看某个元素是否存在于一组元素中,请使用 Set(此方法的标准实现是 HashSet)。如果顺序很重要,但您需要快速查找,请使用 LinkedHashSet,因为它保持插入元素的顺序。两者的查找时间都是 O(1),但在 LinkedHashSet 上插入时间稍长。仅当您实际上是从一个值映射到另一个值时才使用 Map - 如果您只有一组唯一的对象,请使用 Set,如果您有有序的对象,请使用 List。

            【讨论】:

              【解决方案13】:

              site 列出了几种常用(但不那么常用)的数据结构的内存消耗。从那里可以看出HashMap 占用的空间大约是ArrayList 的5 倍。该地图还将为每个条目分配一个额外的对象。

              如果需要可预测的迭代顺序并使用LinkedHashMap,内存消耗会更高。

              您可以使用Memory Measurer 进行自己的内存测量。

              但是有两个重要的事实需要注意:

              1. 很多数据结构(包括ArrayListHashMap)确实分配了比它们当前需要的空间更多的空间,因为否则它们将不得不频繁地执行代价高昂的调整大小操作。因此,每个元素的内存消耗取决于集合中有多少元素。例如,具有默认设置的ArrayList 将相同的内存用于 0 到 10 个元素。
              2. 正如其他人所说,地图的键也被存储。因此,如果它们无论如何都不在内存中,您也必须添加此内存成本。一个额外的对象通常会单独占用 8 个字节的开销,加上其字段的内存,可能还有一些填充。所以这也会占用大量内存。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-02-12
                • 2017-08-30
                • 2020-05-26
                • 2013-03-03
                • 1970-01-01
                • 2014-08-06
                • 2015-10-11
                相关资源
                最近更新 更多