存储在磁盘上的 HashMap 从磁盘读回非常慢

Posted

技术标签:

【中文标题】存储在磁盘上的 HashMap 从磁盘读回非常慢【英文标题】:HashMap stored on disk is very slow to read back from disk 【发布时间】:2011-07-13 11:34:16 【问题描述】:

我有一个存储外部 uid 的 HashMap,然后它存储为给定 uid 设置的不同 id(我们的应用程序内部)。

例如:

123.345.432=00001 123.354.433=00002

通过 uid 检查地图以确保使用相同的内部 id。如果有东西被重新发送到应用程序。

DICOMUID2StudyIdentiferMap定义如下:

private static Map DICOMUID2StudyIdentiferMap = Collections.synchronizedMap(new HashMap());

如果我们成功加载,加载将覆盖它,否则它将使用默认的空HashMap。

通过以下方式从磁盘读回:

FileInputStream f = new FileInputStream( studyUIDFile );  
ObjectInputStream s = new ObjectInputStream( f );

Map loadedMap = ( Map )s.readObject();
DICOMUID2StudyIdentiferMap = Collections.synchronizedMap( loadedMap );

HashMap 使用以下方法写入磁盘:

FileOutputStream f = new FileOutputStream( studyUIDFile );
ObjectOutputStream s = new ObjectOutputStream( f );

s.writeObject(DICOMUID2StudyIdentiferMap);

我遇到的问题是,在 Eclipse 中本地运行的性能很好,但是当应用程序在机器上正常使用时,HashMap 需要几分钟才能从磁盘加载。加载后,通过查看 DICOMUID2StudyIdentiferMap.put(..., ...) 是否会返回值来检查先前的值也需要很长时间。

我在这两种情况下都加载了相同的地图对象,它是一个约 400kb 的文件。它包含的 HashMap 有大约 3000 个键值对。

为什么在一台机器上这么慢,而在 Eclipse 中却没有?

这台机器是一台运行XP的VM,它最近才开始读取HashMap变得缓慢,所以它必须与它的大小有关,但是我认为400kb并不是很大。

欢迎任何建议,TIA

【问题讨论】:

我的建议是使用 jvisualvm 攻击应用程序以找出所有时间都花在了哪里。另一种选择是删除同步包装器,看看情况是否有所改善。 【参考方案1】:

作为@biziclop cmets,您应该首先使用分析器来查看您的应用程序将所有时间都花在了哪里。

如果这没有给你任何结果,这里有几个理论。

可能是您的应用程序即将耗尽堆。当 JVM 接近用完堆时,它几乎可以将所有时间都用于垃圾收集,徒劳地试图继续运行。如果您启用 GC 日志记录,这将显示出来。

可能是 ObjectInputStream 和 ObjectOutputStream 正在执行大量的小型读取系统调用。尝试用缓冲流包装文件流,看看它是否显着加快了速度。

为什么在一台机器上这么慢,而在 Eclipse 中却没有?

“满堆”理论可以解释这一点。 Eclipse 的默认堆大小比使用 java ... 启动且没有堆大小选项的应用程序大得多。

【讨论】:

这很有帮助,谢谢。但是我将 Richard H 的解决方案标记为我的答案,因为我将使用他建议的方法。【参考方案2】:

不确定将 Map 序列化是最佳选择。如果 Map 是基于磁盘的持久性,为什么不使用专为磁盘设计的库?查看Kyoto Cabinet。它实际上是用 c++ 编写的,但有一个 java API。我已经用过好几次了,它非常好用,速度非常快,而且可以扩展到很大的尺寸。

这是我为东京内阁复制/粘贴的一个例子,旧版本的京都,但它基本上是一样的:

import tokyocabinet.HDB;

....

String dir = "/path/to/my/dir/";
HDB hash = new HDB();

// open the hash for read/write, create if does not exist on disk
if (!hash.open(dir + "unigrams.tch", HDB.OWRITER | HDB.OCREAT)) 
    throw new IOException("Unable to open " + dir + "unigrams.tch: " + hash.errmsg());


// Add something to the hash
hash.put("blah", "my string");

// Close it
hash.close();

【讨论】:

【参考方案3】:

这里列出了 122 个 NoSQL 数据库,您可以将其用作替代方案。

这里有两个开销很大的操作,一个是对象的序列化,第二个是磁盘访问。您可以通过仅读取/写入所需数据来加快访问速度。使用自定义格式可以加快连载速度。

您还可以更改数据结构以提高效率。如果您想每次都重新加载/重写整个地图,我建议您使用以下方法。


private Map<Integer, Integer> mapping = new LinkedHashMap<Integer, Integer>();

public void saveTo(File file) throws IOException 
    DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
    dos.writeInt(mapping.size());
    for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) 
        dos.writeInt(entry.getKey());
        dos.writeInt(entry.getValue());
    
    dos.close();


public void loadFrom(File file) throws IOException 
    DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
    mapping.clear();
    int len = dis.readInt();
    for (int i = 0; i < len; i++)
        mapping.put(dis.readInt(), dis.readInt());
    dis.close();


public static void main(String[] args) throws IOException 
    Random rand = new Random();
    Main main = new Main();
    for (int i = 1; i <= 3000; i++) 
        // 100,000,000 to 999,999,999
        int uid = 100000000 + rand.nextInt(900000000); 
        main.mapping.put(uid, i);
    
    final File file = File.createTempFile("deleteme", "data");
    file.deleteOnExit();
    for (int i = 0; i < 10; i++) 
        long start = System.nanoTime();
        main.saveTo(file);
        long mid = System.nanoTime();
        new Main().loadFrom(file);
        long end = System.nanoTime();
        System.out.printf("Took %.3f ms to save and %.3f ms to load %,d entries.%n",
                (end - mid) / 1e6, (mid - start) / 1e6, main.mapping.size());
    

打印

Took 1.203 ms to save and 1.706 ms to load 3,000 entries.
Took 1.209 ms to save and 1.203 ms to load 3,000 entries.
Took 0.961 ms to save and 0.966 ms to load 3,000 entries.

改用 TIntIntHashMap 大约快 10%。

将地图的大小增加到 100 万个打印条目

Took 412.718 ms to save and 62.009 ms to load 1,000,000 entries.
Took 403.135 ms to save and 61.756 ms to load 1,000,000 entries.
Took 399.431 ms to save and 61.816 ms to load 1,000,000 entries.

【讨论】:

【参考方案4】:

也许您应该寻找与Map 类似的替代方法,例如SimpleDB、BerkeleyDB 或 Google BigTable。

【讨论】:

【参考方案5】:

Voldemort 是 Linkedin 流行的开源键值存储。我建议你看看源代码,看看他们是怎么做的。现在我正在查看https://github.com/voldemort/voldemort/blob/master/src/java/voldemort/serialization/ObjectSerializer.java 的序列化部分。查看他们使用ByteArrayOutputStream 的代码,我认为这是从磁盘读取/写入/写入磁盘的更有效方式。

为什么在一台机器上这么慢,而在eclipse中却没有?

您的问题不是很清楚,但是 Eclipse 是否在 VM(VirtualBox?)中运行?因为如果是这样,可能会更快,因为完整的 VM 存储在内存中,这比访问磁盘要快得多。

【讨论】:

【参考方案6】:

我认为这可能是一个哈希问题。您在 Map 中使用的键的类型是什么,它是否有一个有效的 hashCode() 方法可以很好地分散键?

【讨论】:

以上是关于存储在磁盘上的 HashMap 从磁盘读回非常慢的主要内容,如果未能解决你的问题,请参考以下文章

/tmp 与 /dev/shm 用于 Linux 上的临时文件存储?

MySQL索引原理和慢查询优化

精简要了解HDFS网络慢节点和磁盘慢节点监控

核心数据 - 在后台保存到磁盘上的持久存储

DS4700磁盘阵列的控制器微码升级操作记录(收录百度文库)

类加载