大型Java列表的性能较差

大型Java列表的性能较差,java,memory,text,garbage-collection,large-files,Java,Memory,Text,Garbage Collection,Large Files,我正试图用Java将一个大型文本语料库读入内存。在某个时候,它撞到了墙上,垃圾就没完没了地收集起来。我想知道是否有人有过击败Java的GC提交大数据集的经验 我正在读一个8GB的UTF-8格式的英文文本,一行一句。我想split()在空白处的每一行,并将生成的字符串数组存储在ArrayList中,以便进一步处理。下面是一个显示问题的简化程序: /** Load whitespace-delimited tokens from stdin into memory. */ public class

我正试图用Java将一个大型文本语料库读入内存。在某个时候,它撞到了墙上,垃圾就没完没了地收集起来。我想知道是否有人有过击败Java的GC提交大数据集的经验

我正在读一个8GB的UTF-8格式的英文文本,一行一句。我想
split()
在空白处的每一行,并将生成的字符串数组存储在
ArrayList
中,以便进一步处理。下面是一个显示问题的简化程序:

/** Load whitespace-delimited tokens from stdin into memory. */
public class LoadTokens {
    private static final int INITIAL_SENTENCES = 66000000;

    public static void main(String[] args) throws IOException {
        List<String[]> sentences = new ArrayList<String[]>(INITIAL_SENTENCES);
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        long numTokens = 0;
        String line;

        while ((line = stdin.readLine()) != null) {
            String[] sentence = line.split("\\s+");
            if (sentence.length > 0) {
                sentences.add(sentence);
                numTokens += sentence.length;
            }
        }
        System.out.println("Read " + sentences.size() + " sentences, " + numTokens + " tokens.");
    }
}
/**将空格分隔的令牌从stdin加载到内存中*/
公共类装入令牌{
私人静态最终整型初始句子=66000000;
公共静态void main(字符串[]args)引发IOException{
列表句子=新的数组列表(初始句子);
BufferedReader stdin=新的BufferedReader(新的InputStreamReader(System.in));
长numTokens=0;
弦线;
而((line=stdin.readLine())!=null){
字符串[]句子=行。拆分(\\s+);
如果(句子长度>0){
添加(句子);
numTokens+=句子长度;
}
}
System.out.println(“Read”+句子.size()+”句子“+numTokens+”代币“);
}
}
看起来很简单,对吧?你会注意到我甚至预先调整了我的
ArrayList
;我有不到6600万个句子和13亿个代币。现在,如果你拿出你的参考资料和铅笔,你会发现这需要大约:

  • 66e6
    String[]
    references@8字节ea=0.5 GB
  • 66e6
    String[]
    objects@32字节ea=2 GB
  • 66e6
    char[]
    objects@32字节ea=2 GB
  • 1.3e9
    String
    references@8字节ea=10GB
  • 1.3e9
    String
    s@44字节ea=53 GB
  • 8e9
    char
    s@2字节ea=15 GB
83 GB。(您会注意到,我确实需要使用64位对象大小,因为不能帮助我处理>32 GB的堆。)我们很幸运有一台具有128 GB RAM的RedHat 6机器,因此我启动了Java HotSpot(TM)64位服务器VM(构建20.4-b02,混合模式)从我的Java SE 1.6.0_29工具包中,带有
pv giant-file.txt | Java-Xmx96G-Xms96G加载令牌
,只是为了安全起见,在我观看
top
的时候进行回放

在输入不到一半的地方,大约50-60 GB RSS,并行垃圾收集器将启动1300%的CPU(16进程箱),并停止读取进度。然后它会多运行几GB,然后进程会停止更长时间。它的容量达到96 GB,但尚未完成。我让它运行了一个半小时,而它只是在做GC时消耗了约90%的系统时间。这似乎有些极端

为了确保我没有发疯,我快速编写了等价的Python(全部两行代码),并在大约12分钟和70 GB的RSS中运行完成


那么:我是在做傻事吗?(除了通常效率低下的存储方式之外,我真的无能为力——即使我的数据结构很胖,只要它们合适,Java也不应该让人窒息。)对于真正大的堆,有没有神奇的GC建议?我确实试过
-XX:+UseParNewGC
,但似乎更糟。

Idea 1

首先考虑以下几点:

while ((line = stdin.readLine()) != null) {
至少在过去的情况下,
readLine
会返回一个
String
,其背景
char[]
至少包含80个字符。这是否成为问题取决于下一行的功能:

String[] sentence = line.split("\\s+");
您应该确定
split
返回的字符串是否保留相同的备份
char[]

如果他们这样做(并且假设您的行通常短于80个字符),您应该使用:

line = new String(line);
这将使用“大小合适”的字符串数组创建字符串副本的克隆

如果他们不这样做,那么您应该潜在地想出一些方法来创建相同的行为,但要改变它,以便他们使用相同的备份
char[]
(即,它们是原始行的子字符串)-当然,还要执行相同的克隆操作。您不希望每个单词都有一个单独的
char[]
,因为这样会浪费比空格多得多的内存

创意2

您的标题谈到了列表的糟糕性能,但当然,您可以通过简单地创建一个
字符串[][]
,至少出于测试目的,轻松地将列表从等式中去掉。看起来您已经知道了文件的大小,如果不知道,您可以通过
wc
运行该文件,以便事先进行检查。只是想看看你能否从一开始就避免这个问题

创意3


你的语料库中有多少不同的单词?您是否考虑过保留一个
哈希集
,并在遇到它时添加每个单词?这样,您可能会得到更少的字符串。此时,您可能希望放弃第一个想法中的“每行单个backing
char[]
”——您希望每个字符串都有其自己的char数组作为backing,否则一行中只有一个新词仍然需要大量字符。(或者,要进行真正的微调,您可以查看一行中有多少“新词”,并克隆每个字符串。

您应该使用以下技巧:

  • 帮助JVM将相同的令牌收集到单个字符串引用中,这要归功于
    句子.add(句子.intern())
    。有关详细信息,请参阅。据我所知,它还应该具有Jon Skeet所说的效果,它将字符数组切成小块

  • 用于压缩字符串和字符[]实现及相关实现:

    -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat
    
有了这样的内存量,您应该将系统和JVM配置为

仅使用GC调优就很难提高性能,而且超过5%。您应该首先降低应用程序内存消耗
-XX:NewRatio=2 -XX:SurvivorRatio=8
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log