大型Java列表的性能较差
我正试图用Java将一个大型文本语料库读入内存。在某个时候,它撞到了墙上,垃圾就没完没了地收集起来。我想知道是否有人有过击败Java的GC提交大数据集的经验 我正在读一个8GB的UTF-8格式的英文文本,一行一句。我想大型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
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
references@8字节ea=0.5 GBString[]
- 66e6
objects@32字节ea=2 GBString[]
- 66e6
objects@32字节ea=2 GBchar[]
- 1.3e9
references@8字节ea=10GBString
- 1.3e9
s@44字节ea=53 GBString
- 8e9
s@2字节ea=15 GBchar
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
你的语料库中有多少不同的单词?您是否考虑过保留一个
哈希集
,并在遇到它时添加每个单词?这样,您可能会得到更少的字符串。此时,您可能希望放弃第一个想法中的“每行单个backingchar[]
”——您希望每个字符串都有其自己的char数组作为backing,否则一行中只有一个新词仍然需要大量字符。(或者,要进行真正的微调,您可以查看一行中有多少“新词”,并克隆每个字符串。您应该使用以下技巧:
- 帮助JVM将相同的令牌收集到单个字符串引用中,这要归功于
。有关详细信息,请参阅。据我所知,它还应该具有Jon Skeet所说的效果,它将字符数组切成小块句子.add(句子.intern())
- 用于压缩字符串和字符[]实现及相关实现:
-XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat
-XX:NewRatio=2 -XX:SurvivorRatio=8
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log