Ошибка java.lang.OutOfMemoryError: GC overhead limit exceeded — что делать?
Причина ошибки и как её диагностировать
Ошибка java.lang.OutOfMemoryError: GC overhead limit exceeded означает, что сборщик мусора (Garbage Collector) тратит более 98% времени на сборку мусора, но при этом освобождает менее 2% кучи. Фактически JVM сообщает: «Я пытаюсь освободить память, но почти ничего не получается — похоже, мы исчерпали ресурсы». Это не то же самое, что просто «закончилась память» (когда вылетает Java heap space).
В вашем случае проблема не в размере кучи, а в эффективности использования памяти. Вы читаете 500 МБ файл, но HashMap хранит все уникальные слова. Если в файле миллионы уникальных слов (особенно длинных), каждая запись в HashMap занимает десятки байт. Например, 2 миллиона уникальных слов по 10 символов — это уже ~200 МБ только строк, плюс объекты Entry, Integer, массивы — легко уйти в 1+ ГБ. Когда GC пытается очистить временные объекты (строки из split, Integer при merge), а памяти почти нет, возникает цикл: 98% времени — GC, 2% — полезная работа, и JVM прерывает это «зацикливание».
Решение 1: Оптимизация структуры данных
Первое, что нужно сделать — измерить, сколько памяти реально потребляется. Используйте jvisualvm или jconsole. Но сразу можно применить более эффективные структуры:
- Использовать
longвместоIntegerчерез библиотекуfastutilилиHPPC— они хранят примитивы, а не объекты. - Использовать
HashMapс кастомным ключом — если слова повторяются, можно интернировать строки черезString.intern(), но это может ударить по PermGen/Metaspace. - Лучший вариант:
Eclipse CollectionsилиTrove— специализированные коллекции для примитивов.
Пример с Trove (добавьте зависимость):
import gnu.trove.map.hash.TObjectIntHashMap;
TObjectIntHashMap<String> wordCount = new TObjectIntHashMap<>();
// Использование:
wordCount.adjustOrPutValue(word, 1, 1); // аналог merge
Это сократит потребление памяти в 2-3 раза, так как Integer заменён на int.
Решение 2: Потоковая обработка без хранения всех данных
Если вам не нужен полный словарь в памяти (например, для отчёта достаточно топ-1000 слов), используйте ограниченную кучу или внешнюю сортировку:
// Используем TreeMap с ограничением по размеру
Map<String, Integer> topWords = new TreeMap<>();
int maxSize = 10_000; // храним только 10k самых частых слов
while ((line = reader.readLine()) != null) {
String[] words = line.split("\\s+");
for (String word : words) {
topWords.merge(word, 1, Integer::sum);
if (topWords.size() > maxSize) {
// Удаляем наименее частое слово
String minKey = Collections.min(topWords.entrySet(),
Map.Entry.comparingByValue()).getKey();
topWords.remove(minKey);
}
}
}
Это радикально снижает потребление памяти — вы храните только ограниченное количество записей.
Решение 3: Настройка GC (временный костыль)
Если вы уверены, что памяти достаточно, но GC слишком агрессивен, можно отключить проверку лимита:
-XX:-UseGCOverheadLimit
Но это опасно — программа просто зависнет в бесконечном цикле GC, а не выбросит ошибку. Лучше использовать -XX:+UseConcMarkSweepGC или -XX:+UseG1GC — они более эффективны для больших куч.
Рекомендация
Начните с профилирования: запустите с -Xmx1g -XX:+PrintGCDetails и посмотрите логи. Скорее всего, вы увидите, что куча заполняется под завязку. Оптимальное решение — комбинация: используйте TObjectIntHashMap и, если данных много, добавьте внешнюю сортировку (например, записывайте промежуточные результаты в файлы и потом мержите).
Альтернативный подход: Использование базы данных (SQLite или H2)
Если вы не хотите усложнять код коллекциями, но данных действительно много, рассмотрите вариант с лёгкой встраиваемой БД. Это может быть избыточно для учебного проекта, но на практике часто оправдано.
// Пример с H2 (in-memory или file-based)
Connection conn = DriverManager.getConnection("jdbc:h2:file:./wordcount");
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS words (word VARCHAR(255) PRIMARY KEY, count INT)");
PreparedStatement ps = conn.prepareStatement(
"MERGE INTO words (word, count) KEY(word) VALUES(?, 1)");
while ((line = reader.readLine()) != null) {
for (String word : line.split("\\s+")) {
ps.setString(1, word);
ps.executeUpdate();
}
}
Плюсы:
- Не нужно держать всё в памяти — БД использует дисковое хранение.
- Автоматическая индексация и транзакции.
- Можно легко делать сложные запросы (топ-100, фильтрация).
- Медленнее, чем HashMap (дисковый I/O).
- Нужна зависимость (H2 весит ~2 МБ).
- Избыточно для простой задачи подсчёта слов.
Этот вариант стоит применять, когда объём данных превышает доступную RAM (например, 10+ ГБ логов) и требуется надёжное хранение промежуточных результатов. Для вашего случая с 500 МБ файлом лучше сначала попробовать оптимизировать коллекции — это проще и быстрее.