![](/img/trans.png)
[英]memory reallocation issue in Java ArrayList, HashSet and HashMap
[英]Memory overhead of Java HashMap compared to ArrayList
我想知道java HashMap与ArrayList相比的内存开销是多少?
更新:
我想提高搜索大包(600万+)相同对象的特定值的速度。
因此,我正在考虑使用一个或多个HashMap而不是使用ArrayList。 但我想知道HashMap的开销是多少。
据我所知,密钥不是存储的,只是密钥的散列,所以它应该像对象的散列大小+一个指针 。
但是使用了什么哈希函数? 它是Object提供的还是另一个?
如果您将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单独存储对键和值的引用。
注意事项:
用于HashMap键的最常见类型是String。 对象创建开销不适用于此处,因此差异会更小。
我有一个2.8的数字,插入到ArrayList中的8880502条目与3148004插入-Xmx256M JVM上的HashMap,但是我的ArrayList加载因子是80%而且我的对象非常小--12个字节加上8个字节的对象开销。
我的图和我的实现要求密钥包含在值中,否则我会遇到与对象创建开销相同的问题,它只是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);
}
}
最简单的方法是查看源代码并以此方式进行处理。 但是,你真的在比较苹果和橘子 - 列表和地图在概念上非常不同。 您很少根据内存使用情况在它们之间进行选择。
这个问题背后的背景是什么?
所有存储在其中的都是指针。 根据您的体系结构,指针应为32位或64位(或更多或更少)
10的数组列表倾向于至少分配10个“指针”(以及一些一次性开销的东西)。
地图必须分配两次(20个指针),因为它一次存储两个值。 然后,最重要的是,它必须存储“哈希”。 它应该大于地图,在75%的负载下它应该是大约13个32位值(散列)。
所以,如果你想要一个随便的答案,比例应该是大约1:3.25左右,但你只是在谈论指针存储 - 非常小,除非你存储大量的对象 - 如果是这样,能够实现即时引用(HashMap)vs iterate(数组)应该比内存大小更重要。
哦,还有:阵列可以适合您收藏的确切尺寸。 如果你指定大小,HashMaps也可以,但如果它“超出”那个大小,它将重新分配一个更大的数组而不使用它的一些,所以也可能有一些浪费。
我也没有给你一个答案,但快速谷歌搜索在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()中减去。
再次,抱歉编辑你的帖子!
Hashmaps尝试维护加载因子(通常为75%已满),您可以将hashmap视为稀疏填充的数组列表。 直接比较大小的问题是地图的这个加载因子增长以满足数据的大小。 另一方面,ArrayList通过将其内部数组大小加倍来增长以满足其需求。 对于相对较小的大小,它们是可比较的,但是当您将越来越多的数据打包到地图中时,它需要大量空引用以保持散列性能。
在任何一种情况下,我建议在开始添加之前启动数据的预期大小。 这将为实现提供更好的初始设置,并且在两种情况下都可能消耗更少。
更新:
根据您更新的问题,查看Glazed列表 。 这是一些由Google的一些人编写的简洁工具,用于执行与您描述的操作类似的操作。 它也很快。 允许群集,过滤,搜索等
HashMap保存对值的引用和对键的引用。
ArrayList只保存对该值的引用。
因此,假设密钥使用相同的内存值,HashMap使用的内存增加了50%(尽管严格来说,不是使用该内存的HashMap,因为它只保留对它的引用)
另一方面,HashMap为基本操作(get和put)提供了恒定时间性能。因此,虽然它可能使用更多内存,但使用HashMap获取元素可能比使用ArrayList快得多。
所以,你应该做的下一件事是不关心谁使用更多的内存,但他们有什么好处 。
为程序使用正确的数据结构可以节省比在其下实现库的方式更多的CPU /内存。
编辑
在Grant Welch回答之后,我决定测量2,000,000个整数。
这是源代码
这是输出
$
$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
基本上,你应该使用“正确的工具”。 由于有不同的实例,您需要一个键/值对(您可以使用HashMap
)和不同的实例,您只需要一个值列表(您可以使用ArrayList
),然后问题是“哪个一个人使用更多的记忆“,在我看来,是没有实际意义的,因为它不是考虑选择一个而不是另一个。
但回答这个问题,由于HashMap
存储键/值对,而ArrayList
只存储值,我认为单独向HashMap添加键意味着它会占用更多内存,当然,假设我们通过它们来比较它们相同的值类型 (例如,两者中的值都是字符串)。
我认为这里有一个错误的问题。
如果你想改善你可以搜索一个物体的速度List
包含六个万个条目,那么你应该看看这些数据类型的检索操作的速度有多快执行。
像往常一样,这些类的Javadoc很清楚地表明了它们提供的性能类型:
HashMap :
假设散列函数在桶之间正确地分散元素,该实现为基本操作(get和put)提供了恒定时间性能。
这意味着HashMap.get(key)是O(1)
。
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
进行迭代将不会非常有效。
我不知道确切的数字,但HashMaps更重。 比较这两者,ArrayList的内部表示是不言而喻的,但HashMaps保留了Entry对象(Entry),这可以增加你的内存消耗。
它不是那么大,但它更大。 一个可视化的好方法是使用动态分析器,例如YourKit ,它允许您查看所有堆分配。 这很不错。
这篇文章提供了很多关于Java中对象大小的信息。
正如Jon Skeet所说,这些是完全不同的结构。 地图(例如HashMap)是从一个值到另一个值的映射 - 即,您有一个映射到值的键,在Key-> Value类型的关系中。 密钥是经过哈希处理的,并且放在一个数组中以便快速查找。
另一方面,List是具有顺序的元素的集合--ArrayList碰巧使用数组作为后端存储机制,但这是无关紧要的。 每个索引元素都是列表中的单个元素。
编辑:根据您的评论,我添加了以下信息:
密钥存储在hashmap中。 这是因为不保证散列对于任何两个不同的元素是唯一的。 因此,必须在散列冲突的情况下存储密钥。 如果您只是想查看一组元素中是否存在元素,请使用Set(此标准实现为HashSet)。 如果订单很重要,但您需要快速查找,请使用LinkedHashSet,因为它保持元素的插入顺序。 两者的查找时间均为O(1),但LinkedHashSet的插入时间稍长。 仅当您实际从一个值映射到另一个值时才使用Map - 如果您只有一组唯一对象,则使用Set,如果您有已排序的对象,则使用List。
如果您正在考虑两个ArrayLists与一个Hashmap,那么它是不确定的; 两者都是部分完整的数据结构。 如果你比较Vector和Hashtable,Vector可能更有效,因为它只分配它使用的空间,而Hashtables分配更多的空间。
如果你需要一个键值对并且没有做出令人难以置信的内存需求,那么只需使用Hashmap即可。
该站点列出了几种常用(并非常见)使用的数据结构的内存消耗。 从那里可以看出HashMap
大约是ArrayList
空间的5倍。 地图还将为每个条目分配一个额外的对象。
如果您需要可预测的迭代顺序并使用LinkedHashMap
,则内存消耗将更高。
您可以使用Memory Measurer进行自己的内存测量。
但是有两个重要的事实需要注意:
ArrayList
和HashMap
)确实为空间分配了比当前需要更多的空间,因为否则它们必须经常执行昂贵的调整大小操作。 因此,每个元素的内存消耗取决于集合中有多少元素。 例如,具有默认设置的ArrayList
对0到10个元素使用相同的内存。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.