Java HashMap的内存开销与ArrayList相比

时间:2009-10-06 16:14:04

标签: java arraylist hashmap memory-management

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

更新

我想提高搜索大包装(600万+)相同物品的特定值的速度。

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

据我所知,密钥不存储,只存储密钥的哈希值,所以它应该是对象哈希值的大小+一个指针

但是使用了什么哈希函数?是the one offered by Object还是其他人?

13 个答案:

答案 0 :(得分:42)

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

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

需要注意的关键事项是:

(a)它不是引用所需的空间或“加载因子”,而是创建对象所需的开销。如果密钥是基本类型,或者是2个或更多基元或引用值的组合,则每个密钥都需要自己的对象,其中包含8个字节的开销。

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

注意事项:

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

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

  3. 我的图和我的实现要求密钥包含在值中,否则我会遇到与对象创建开销相同的问题,它只是HashMap的另一个实现。

    < / LI>

    我的代码:

    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);
        }
    }
    

答案 1 :(得分:15)

最简单的方法是查看源代码并以此方式解决问题。但是,你真的在​​比较苹果和橘子 - 列表和地图在概念上非常不同。您很少根据内存使用情况在它们之间进行选择。

这个问题背后的背景是什么?

答案 2 :(得分:8)

存储在其中的任何一个都是指针。根据您的体系结构,指针应为32或64位(或更多或更少)

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

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

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

哦,还有: 数组可以适合您的集合的确切大小。如果您指定大小,HashMaps也可以,但如果它“超出”大小,它将重新分配更大的数组而不使用其中的一些,所以也可能有一些浪费。

答案 3 :(得分:7)

我也没有给你一个答案,但快速谷歌搜索在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()中减去。

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

答案 4 :(得分:3)

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

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

<强>更新

根据您更新的问题结帐Glazed lists。这是一些由Google的一些人编写的简洁工具,用于执行与您描述的操作类似的操作。它也很快。允许群集,过滤,搜索等。

答案 5 :(得分:3)

HashMap 持有对该值的引用和对该键的引用。

ArrayList 只需保存对该值的引用。

因此,假设密钥使用相同的内存值,HashMap使用的内存增加了50%(尽管严格来说,不是使用该内存的HashMap,因为它只是保留了对它的引用)

另一方面,HashMap为基本操作(get和put)提供常量性能因此,虽然它可能使用更多内存,但使用HashMap获取元素可能比使用HashMap快得多ArrayList中。

所以,你应该做的下一件事是不关心谁使用更多内存但是有什么好处。

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

编辑

在Grant Welch的回答之后,我决定测量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

答案 6 :(得分:2)

基本上,你应该使用“正确的工具”。由于存在不同的实例,您需要一个键/值对(您可以使用HashMap)和不同的实例,您只需要一个值列表(您可以使用ArrayList然后,在我看来,“哪一个使用更多记忆”的问题没有实际意义,因为它不是考虑选择一个而不是另一个。

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

答案 7 :(得分:2)

我认为这里有一个错误的问题。

如果您希望提高搜索包含六百万个条目的List中对象的速度,那么您应该研究这些数据类型的检索操作执行的速度

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

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进行迭代将不会非常有效。

答案 8 :(得分:1)

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

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

答案 9 :(得分:1)

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

答案 10 :(得分:0)

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

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

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

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

答案 11 :(得分:0)

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

如果你需要一个键值对,并且没有做出令人难以置信的内存需求,只需使用Hashmap。

答案 12 :(得分:0)

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

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

您可以使用Memory Measurer进行自己的记忆测量。

但有两个重要事实要注意:

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