使用Java创建NGram模型时遭遇Java堆内存溢出问题求助
问题背景
在大型项目的指定模块中,需用Java构建NGram模型(方案为必需项),使用JDK 20 + VS Code运行代码时持续出现堆内存溢出错误。已将堆内存调整至8GB、10GB甚至12GB,但问题仍未解决;任务管理器显示内存仅占用4-5GB,远未达到分配上限,推测问题出在数组等数据结构的内存占用逻辑上。
错误日志
初始堆内存8GB时的错误
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.HashMap.resize(HashMap.java:710)
at java.base/java.util.HashMap.putVal(HashMap.java:635)
at java.base/java.util.HashMap.put(HashMap.java:618)
at Ngram.NGramNode.addNGram(NGramNode.java:277)
at Ngram.NGramNode.addNGram(NGramNode.java:280)
at Ngram.NGram.addNGramSentence(NGram.java:157)
at com.glmadu.editdistance.TRspellChecker.getCorpus(TRspellChecker.java:68)
at com.glmadu.editdistance.TRspellChecker.checkFileSpell(TRspellChecker.java:22)
at com.glmadu.App.main(App.java:21)
调整至10GB/12GB时的错误
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space: failed reallocation of scalar replaced objects
at java.base/java.lang.String.split(String.java:3138)
at java.base/java.lang.String.split(String.java:3212)
at com.glmadu.editdistance.TRspellChecker.getCorpus(TRspellChecker.java:63)
at com.glmadu.editdistance.TRspellChecker.checkFileSpell(TRspellChecker.java:22)
at com.glmadu.App.main(App.java:21)
相关代码片段
private static void getCorpus(String output) { ArrayList<ArrayList<String>> corpus = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader("path/to/corpus"))) { String line; while ((line = br.readLine()) != null) { String[] tokens = line.split(" "); //line 63 ArrayList<String> sentence = new ArrayList<>(); for (String token : tokens) { sentence.add(token); // line 68 } corpus.add(sentence); } NGram<String> nGram = new NGram<>(corpus, 2); nGram.saveAsText(output); } catch (IOException e) { e.printStackTrace(); } }
已尝试无效方案
- 多次增大堆内存至8GB、10GB、12GB
- 仅读取语料库前1000行
- 跳过NGram模型保存步骤
解决方案
1. 逐行处理语料,避免全量加载
当前代码将所有句子存入corpus集合后才构建NGram,导致内存积压大量冗余数据。改为逐行处理并即时更新NGram模型,处理完成后立即释放该行内存:
private static void getCorpus(String output) { // 直接初始化NGram,无需预存所有句子 NGram<String> nGram = new NGram<>(2); try (BufferedReader br = new BufferedReader(new FileReader("path/to/corpus"))) { String line; while ((line = br.readLine()) != null) { String[] tokens = line.split(" "); ArrayList<String> sentence = new ArrayList<>(); for (String token : tokens) { sentence.add(token); } // 处理完一行就添加到NGram,sentence会被GC自动回收 nGram.addNGramSentence(sentence); } nGram.saveAsText(output); } catch (IOException e) { e.printStackTrace(); } }
注:需确保NGram类支持动态添加句子,若原构造函数要求一次性传入全部语料,需新增addNGramSentence方法。
2. 字符串去重减少内存占用
语料库中大量重复token会占用额外内存,使用字符串池复用相同字符串:
// 方法1:使用JVM内置字符串池 sentence.add(token.intern()); // 方法2:自定义线程安全字符串池 private static final Map<String, String> STRING_POOL = new ConcurrentHashMap<>(); // ... sentence.add(STRING_POOL.computeIfAbsent(token, k -> k));
3. 优化字符串分割逻辑
String.split(" ")在处理连续空格时会生成空字符串,且内存效率较低。改用StringTokenizer跳过空白字符:
StringTokenizer tokenizer = new StringTokenizer(line); ArrayList<String> sentence = new ArrayList<>(); while (tokenizer.hasMoreTokens()) { sentence.add(tokenizer.nextToken().intern()); }
4. 验证JVM内存配置是否生效
确认VS Code的launch.json中堆内存配置格式正确:
{ "configurations": [ { "type": "java", "name": "App", "request": "launch", "mainClass": "com.glmadu.App", "vmArgs": "-Xmx8g -Xms4g" // 明确设置初始堆和最大堆 } ] }
也可在代码中打印堆内存配置验证:
Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory() / (1024 * 1024); System.out.println("Max Heap Memory: " + maxMemory + "MB");
5. 排查大对象与内存泄漏
使用JDK自带工具分析堆内存:
- 导出堆快照:
jmap -dump:format=b,file=heapdump.hprof <进程PID> - 分析堆快照:
jhat heapdump.hprof
查看是否存在超长字符串、超大数组等大对象,或集合未及时释放引用导致的内存泄漏。
内容的提问来源于stack exchange,提问作者klavas

