Avatar ·

Ошибка java.lang.OutOfMemoryError: GC overhead limit exceeded — что делать?

Всем привет! Я начинающий Java-разработчик, пишу небольшое приложение для обработки логов. Программа читает большой текстовый файл (около 500 МБ), разбивает строки на слова и собирает статистику — сколько раз каждое слово встречается. Использую HashMap. Всё работало нормально на маленьких файлах, но когда я запустил на реальных данных, через пару минут вылетела ошибка: Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded Вот упрощённый код: ```java public class LogAnalyzer { public static void main(String[] args) throws IOException { Map wordCount = new HashMap<>(); BufferedReader reader = new BufferedReader(new FileReader("large_log.txt")); String line; while ((line = reader.readLine()) != null) { String[] words = line.split("\\s+"); for (String word : words) { wordCount.merge(word, 1, Integer::sum); } } reader.close(); System.out.println("Уникальных слов: " + wordCount.size()); } } ``` Пробовал увеличить размер кучи через -Xmx2g — не помогло, ошибка осталась. Что я делаю не так? Может, нужно использовать другую структуру данных или GC настроить? Буду благодарен за подробное объяснение! <<>>

Причина ошибки и как её диагностировать

Ошибка 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 МБ файлом лучше сначала попробовать оптимизировать коллекции — это проще и быстрее.