Java Collectors.toMap的内存优化

Jus*_*meo 10 java java-8 java-stream

我有一个将列表转换为地图的功能.调用此函数后,映射的大小不会更改.我正在尝试在以下两个实现之间做出决定:

Map<Long, Object> listToMap(List<Object> objs) {
        /* Implementation One: */

        Map<Long, Object> map = new HashMap<>(objs.size(), 1);
        for (Object obj : objs) {
            map.put(obj.getKey(), obj);
        }
        return map;

        /* Implementation Two: */

        return objs.stream().collect(Collectors.toMap(Object::getKey, obj -> obj));

    }
Run Code Online (Sandbox Code Playgroud)

在第一个实现中,我通过使用1的加载因子和列表的大小为所有元素分配了足够的内存.这确保不会执行调整大小操作.然后,我遍历列表并逐个添加元素.

在第二个实现中,我使用Java 8流来提高可读性.

我的问题是:第二个实现是否会涉及HashMap的多个调整大小,还是已经过优化以分配足够的内存?

tha*_*guy 8

第二个实现将涉及HashMap的多个调整大小.

我通过在调试器中运行它来确定这一点,并且每次调整哈希映射的大小时都会中断.首先,我调整了您发布的代码,以便在我的系统上进行编译:

import java.util.*;
import java.util.stream.*;

class Test {
  public static void main(String[] args) {
    List<Object> list = new ArrayList<Object>();
    for(int i=0; i<100000; i++) {
      list.add(new Integer(i));
    }
    new Test().listToMap(list);
  }

    Map<Integer, Object> listToMap(List<Object> objs) {
        return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我编译它并在调试器中运行它直到它命中listToMap:

$ javac Test.java && jdb Test
Initializing jdb ...
> stop in Test.listToMap
Deferring breakpoint Test.listToMap.
It will be set after the class is loaded.
> run
run Test
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Test.listToMap

Breakpoint hit: "thread=main", Test.listToMap(), line=14 bci=0
14            return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));

main[1]
Run Code Online (Sandbox Code Playgroud)

然后我设置了一个断点java.util.HashMap.resize并继续:

main[1] stop in java.util.HashMap.resize
Set breakpoint java.util.HashMap.resize
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1]
Run Code Online (Sandbox Code Playgroud)

并且cont在我感到无聊之前又多了一些:

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), 
line=678 bci=0

main[1] print size
 size = 3073
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] print size
 size = 6145
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] print size
 size = 12289
Run Code Online (Sandbox Code Playgroud)

所以是的:它肯定会不断调整大小.


Ste*_*n C 7

第二个实现是否涉及HashMap的多个调整大小,还是已经过优化以分配足够的内存?

在你的代码中,前者.请参阅/sf/answers/3593377301/

值得注意的是,对于您当前的实施:

  1. 调整大小所消耗的大部分额外内存将在下一次GC运行时回收.
  2. 在后collect结束,你仍然可以结束了一个主哈希数组,它是高达2倍太大."浪费"的内存可能在表中每个条目最多8个字节,但平均每个条目将是4个字节.
  3. 即便如此,散列入口节点将是a中最大的内存消费者HashMap.除了用于表示键和值的空间外,每个条目大约消耗32个字节.

(以上数字假定为64位引用.)


作为替代方案,如果使用4参数重载,则toMap()可以提供一个Supplier来创建Map要填充的内容.这允许您执行以下操作:

  • HashMap使用足够大的初始容量来分配a 以避免调整大小,但不能太大.
  • 使用(假设的)替代实现,Map每个条目使用的内存少于HashMap.
  • 创建一个包装器来填充类似地图的对象,该对象不会Map<K,V>为您的KV类型实现.... (例如,您可以使用TLongObjectHashMapGNU Trove库.)

(在后两种情况下,目标是找到一个Map或"类似地图"的类,它使用较少的内存(对于你的KV类型)但仍具有适当的查找性能.)