【问题标题】:Storing and comparing a large quantity of Strings in Java在 Java 中存储和比较大量字符串
【发布时间】:2015-08-05 13:26:24
【问题描述】:

我的应用程序在一个 ArrayList 中存储了大量(大约 700,000 个)字符串。字符串是从这样的文本文件中加载的:

        List<String> stringList = new ArrayList<String>(750_000);

        //there's a try catch here but I omitted it for this example
        Scanner fileIn = new Scanner(new FileInputStream(listPath), "UTF-8");
        while (fileIn.hasNext()) {
            String s = fileIn.nextLine().trim();

            if (s.isEmpty()) continue;
            if (s.startsWith("#")) continue;   //ignore comments

            stringList.add(s);
        }
        fileIn.close();

稍后,使用此代码将其他字符串与此列表进行比较:

    String example = "Something";
    if (stringList.contains(example))
        doSomething();

这种比较会发生数百(数千?)次。


这一切都有效,但我想知道是否有什么我可以做的来使它变得更好。我注意到当加载 700K 字符串时,JVM 的大小从大约 100MB 增加到 600MB。字符串主要是这个大小:

Blackened Recordings 
Divergent Series: Insurgent 
Google 
Pixels Movie Money 
X Ambassadors 
Power Path Pro Advanced 
CYRFZQ

我可以做些什么来减少内存,还是可以预料到的?一般有什么建议吗?

【问题讨论】:

  • contains 是非常慢的方法(O(n)
  • JEP 254 / java 9 会带来紧凑的字符串;您可以在那里阅读一些关于字符串内存消耗和运行时性能的想法。
  • 特里可以帮助你
  • @fge 这个问题,他想减少内存使用,为什么不用streamfilter不存储文件内容?
  • @chengpohi 表演!一个 trie 将比这里的列表占用更少的内存

标签: java string list memory


【解决方案1】:

ArrayList 是内存有效的。您的问题可能是由 java.util.Scanner 引起的。 Scanner 在解析过程中创建了大量临时对象(模式、匹配器等),不适合大文件。

尝试用java.io.BufferedReader替换它:

List<String> stringList = new ArrayList<String>();
BufferedReader fileIn = new BufferedReader(new FileReader("UTF-8"));
String line = null;
while ((line = fileIn.readLine()) != null) {
    line = line.trim();

    if (line.isEmpty()) continue;
    if (line.startsWith("#")) continue;   //ignore comments

    stringList.add(line);
}
fileIn.close();

java.util.Scanner source code

要查明内存问题,请将任何内存分析器附加到您的 JVM,例如 VisualVM from JDK tools

添加:

让我们做一些假设:

  1. 您有 700000 个字符串,每个字符串 20 个字符。
  2. 对象引用大小为 32 位,对象头 - 24,数组头 - 16,char - 16,int 32。

那么每个字符串将消耗 24+32*2+32+(16+20*16) = 456 位。

带有字符串对象的整个 ArrayList 将消耗大约 700000*(32*2+456) = 364000000 位 = 43.4 MB(非常粗略)。

【讨论】:

  • 是的,你是对的,原来的问题是关于内存消耗的。
  • ArrayList 是内存有效的。可能大多数对象分配都是由“java.util.Scanner”引起的,它在解析过程中分配了很多 Mather 和 Pattern。 (见源代码:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…)。
  • @Viktor 你提到了一个好点。 OP 应该更详细地确定内存问题。
  • 我也将尝试使用 HashSet/HashMap 的其他建议,但使用 BufferedReader 会产生令人难以置信的不同。它只增加了 70MB 而不是 500MB。我的程序现在使用了大约 189MB,而之前使用了 600MB。谢谢@Viktor!
【解决方案2】:

不完全是一个答案,但是:
您的场景在我的机器上使用了大约 70mb:

long usedMemory = -(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
{//
    String[] strings = new String[700_000];
    for (int i = 0; i < strings.length; i++) {
        strings[i] = new String(new char[20]);
    }
}//
usedMemory += Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println(usedMemory / 1_000_000d + " mb");

你是如何达到 500mb 的?据我所知,String 内部有一个char[],每个char 有16 位。考虑到ObjectString 开销,500mb 对于字符串来说仍然是相当多的。您可以在您的机器上执行一些基准测试。

正如其他人已经提到的,您应该更改元素查找/比较的数据结构。

【讨论】:

  • 假设对象引用占用 32 位,对象头 - 24,数组头 - 16,char -16,int - 32。20 个字符的字符串将包含 2 个 int 字段,1 个对 char 的对象引用数组:16+16+32+16+20*16 = 400 位。由数组支持的 ArrayList 可以比 ArraList 大小大两倍:[一些 ArrayList 字段,不关心] + 700'000*2*32 + 400*70000 ~ 324800000 bits ~ 40 MB。跨度>
【解决方案3】:

使用HashSet 而不是ArrayList 可能会更好,因为addcontains 都是HashSet 中的常数时间操作。

但是,它确实假设您的对象的 hashCode 实现(它是 Object 的一部分,但可以被覆盖)是均匀分布的。

【讨论】:

    【解决方案4】:

    有一个 Trie 数据结构可以用作字典,有这么多的字符串,它们可以出现多次。 https://en.wikipedia.org/wiki/Trie 。它似乎适合你的情况。

    更新: 例如,如果您希望出现字符串,则可以选择 HashSetHashMap 字符串 -> 某些东西。散列集合肯定会比列表快。

    我会从 HashSet 开始。

    【讨论】:

    • HashSet 在这里比HashMap 更有意义,因为没有相关数据
    • 是的,我修改了帖子,最近我在做类似案例的事情并且写的更新太快了;)。谢谢
    【解决方案5】:

    使用ArrayList 对您的用例来说是一个非常糟糕的主意,因为它没有排序,因此您无法有效地搜索条目。

    最适合您的情况的内置类型是 a 是 TreeSet&lt;String&gt;。它保证 add()contains() 的 O(log(n)) 性能。

    请注意,TreeSet 在基本实现中不是线程安全的。使用 mt-safe 包装器(请参阅 TreeSet 的 JavaDocs)。

    【讨论】:

    • OP 想要检查是否存在/包含,而不是排序。散列集更快。我担心排序数组在最坏的情况下会是相同的 O(n) 时间
    • exist/contains 只有在对数据进行排序时才能快速运行。这就是排序很重要的原因。顺便说一句,TreeSet 不需要额外的 sort() 调用,但每次插入时都会对集合进行排序。
    • 真的吗?:) 那么为什么散列集合没有排序并且包含 O(1) 而树集包含 O(log(n)) ?
    【解决方案6】:

    这是一种 Java 8 方法。它使用利用 Stream API 的Files.lines() 方法。此方法将文件中的所有行作为 Stream 读取。 因此,在终端操作之前不会创建任何 String 对象,这是一个静态方法 MyExecutor.doSomething(String)

    /**
    * Process lines from a file.
    * Uses Files.lines() method which take advantage of Stream API introduced in Java 8.
    */
    private static void processStringsFromFile(final Path file) {
     try (Stream<String> lines = Files.lines(file)) {
       lines.map(s -> s.trim())
         .filter(s -> !s.isEmpty())
         .filter(s -> !s.startsWith("#"))
         .filter(s -> s.contains("Something"))
         .forEach(MyExecutor::doSomething);
     } catch (IOException ex) {
         logProcessStringsFailed(ex);        
     }
    }
    

    我在 NetBeans 中执行了Analysis of Memory Usage,这里是 doSomething() 的空实现的内存结果

    public static void doSomething(final String s) {
    
    }
    

    实时字节 = 6702720 ≈ 6.4MB

    【讨论】:

      猜你喜欢
      • 2014-02-20
      • 1970-01-01
      • 1970-01-01
      • 2014-05-11
      • 1970-01-01
      • 2011-04-22
      • 2014-11-11
      • 2012-06-28
      • 1970-01-01
      相关资源
      最近更新 更多