⚡ Раздел 7 · Вопрос #7

В чём разница между Error и Exception

Оба класса — java.lang.Error и java.lang.Exception — являются прямыми наследниками класса java.lang.Throwable, однако они представляют два принципиально разных типа проблем.


🟢 Junior Level

Оба класса — java.lang.Error и java.lang.Exception — являются прямыми наследниками класса java.lang.Throwable, однако они представляют два принципиально разных типа проблем.

Главные различия

Критерий java.lang.Error java.lang.Exception
Природа проблемы Катастрофический сбой виртуальной машины JVM или нехватка системных ресурсов ОС. Нештатная ситуация в логике приложения или временный сбой окружения.
Возможность восстановления Невосстановимо. Приложение не способно программно устранить причину сбоя. Восстановимо. Программа может перехватить ошибку и продолжить работу.
Контроль компилятора Непроверяемое (Unchecked). Делится на Checked (IOException) и Unchecked (RuntimeException).
Правило обработки Не перехватывать. Процесс должен быть аварийно завершен. Обрабатывать локально в try-catch или централизованно.
Типичные примеры OutOfMemoryError, StackOverflowError, NoClassDefFoundError. NullPointerException, IllegalArgumentException, IOException, SQLException.

Наглядные примеры

// 1. Пример Error: исчерпание памяти JVM (OutOfMemoryError)
// Приложение бессильно: свободной памяти в Heap физически больше нет
List<byte[]> memoryLeak = new ArrayList<>();
while (true) {
    memoryLeak.add(new byte[1024 * 1024]); // Выделение по 1 МБ
}

// 2. Пример Exception: отсутствие файла (IOException)
// Приложение может легко восстановиться, прочитав дефолтный файл
try {
    return Files.readString(Path.of("custom-config.json"));
} catch (IOException ex) {
    return Files.readString(Path.of("default-config.json"));
}

🟡 Middle Level

Разбор классической пары: NoClassDefFoundError vs ClassNotFoundException

Один из самых частых вопросов на технических интервью по Core Java:

                            java.lang.Throwable
                                     │
                    ┌────────────────┴────────────────┐
                    ▼                                 ▼
           java.lang.Error                   java.lang.Exception
                    │                                 │
          LinkageError                       ClassNotFoundException
                    │                          (Checked Exception)
          NoClassDefFoundError
  1. ClassNotFoundException (Checked Exception):
    • Возникает при динамической загрузке класса по строковому имени в runtime: Class.forName("com.mysql.cj.jdbc.Driver"), ClassLoader.loadClass().
    • Означает: в текущем ClassLoader класс с таким именем физически отсутствует в classpath.
    • Метод обязан объявить throws ClassNotFoundException, вызывающий код может обработать ошибку.
  2. NoClassDefFoundError (Error):
    • Возникает, когда класс присутствовал на этапе компиляции, но исчез из classpath в момент выполнения.
    • Другой частый сценарий: при первой попытке загрузки класса упал его статический блок инициализации (static { ... }), выбросив ExceptionInInitializerError. При всех последующих обращениях к этому классу в том же ClassLoader JVM будет выбрасывать NoClassDefFoundError, так как класс помечен как непригодный к использованию.

Разновидности OutOfMemoryError (OOM)

Ошибка OutOfMemoryError не монолитна и делится на ключевые подвиды:

  • OutOfMemoryError: Java heap space: Переполнена основная куча (Eden + Survivor + Old Gen). Сборщик мусора (GC) не смог освободить память. Причина: утечка ссылок в коллекциях, тяжелые кэши.
  • OutOfMemoryError: Metaspace: Исчерпана нативная память под метаданные классов (Class metadata). Причина: бесконечная генерация динамических прокси (CGLIB, Spring AOP, Hibernate) без выгрузки ClassLoader.
  • OutOfMemoryError: GC overhead limit exceeded: GC тратит более 98% процессорного времени на сборку мусора, но освобождает менее 2% кучи.
  • OutOfMemoryError: Unable to create new native thread: Достигнут лимит операционной системы на количество потоков для процесса (ulimit -u в Linux).

StackOverflowError vs OutOfMemoryError

  • StackOverflowError: Исчерпана память стека конкретного потока (размер задается флагом -Xss, по умолчанию 1 МБ). Причина: бесконечная или чрезмерно глубокая рекурсия.
  • OutOfMemoryError: Исчерпана память кучи (-Xmx) или нативная память хоста.

🔴 Senior Level

Почему перехватывать Error — это грубая архитектурная ошибка?

Когда в потоке возникает java.lang.Error (в частности, подклассы VirtualMachineError):

  1. Нарушение инвариантов памяти: Поток мог быть прерван посреди выполнения модификации разделяемой структуры данных (например, ConcurrentHashMap).
  2. Разрушение мониторов: Блокировки synchronized снимаются автоматически при размотке стека, оставляя объект в полуразрушенном несогласованном состоянии.
  3. Эффект «Зомби-процесса»: Если поймать OutOfMemoryError через catch (Throwable t), приложение продолжит висеть в памяти. Оно перестанет отвечать на входящие запросы, но не упадет, из-за чего Kubernetes не сможет перезапустить неисправный под.

Паттерн “Crash-Only Software” и флаги JVM

В современных облачных архитектурах (Cloud-Native / Kubernetes) действует принцип: при критической ошибке процесс должен упасть мгновенно (Fail-Fast & Crash), чтобы оркестратор запустил чистый инстанс.

Настройки JVM в production для контейнеров:

# 1. Мгновенно аварийно завершить JVM при OOM (возврат ненулевого exit code)
-XX:+CrashOnOutOfMemoryError

# 2. Либо завершить процесс через мгновенный выход
-XX:+ExitOnOutOfMemoryError

# 3. Сохранить дамп кучи для последующего расследования утечки памяти
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/dumps/oom.hprof

Демонстрация: как падение в static block порождает NoClassDefFoundError

package com.example.error;

public class ErrorDemo {

    // Класс с дефектным статическим блоком
    static class BrokenClass {
        static {
            // Вызовет ArithmeticException внутри static инициализатора
            int badDivision = 10 / 0;
        }

        public static void test() {
            System.out.println("Hello");
        }
    }

    public static void main(String[] args) {
        // 1-я попытка: выбросит ExceptionInInitializerError
        try {
            BrokenClass.test();
        } catch (Throwable t) {
            System.out.println("First call failed: " + t.getClass().getSimpleName()); 
            // Выведет: ExceptionInInitializerError
        }

        // 2-я попытка: класс уже помечен JVM как ошибочный -> выбросит NoClassDefFoundError!
        try {
            BrokenClass.test();
        } catch (Throwable t) {
            System.out.println("Second call failed: " + t.getClass().getSimpleName()); 
            // Выведет: NoClassDefFoundError!
        }
    }
}

🎯 Шпаргалка для интервью

30-секундный ответ

  • Error обозначает критические, невосстановимые системные сбои JVM или нехватку ресурсов операционной системы (OutOfMemoryError, StackOverflowError, NoClassDefFoundError). Прикладной код не должен перехватывать Error, так как среда выполнения переходит в нестабильное состояние; процесс должен немедленно завершиться, чтобы оркестратор (Kubernetes) перезапустил под.
  • Exception обозначает предсказуемые ошибки уровня приложения (IOException, NullPointerException, бизнес-ошибки), которые программа может и обязана обработать.
  • ClassNotFoundException — это checked-исключение при явном поиске класса по имени в Class.forName(), тогда как NoClassDefFoundError — это фатальный Error, возникающий при сбое линковки (класс пропал из classpath в runtime или упал статический блок static {}).

4 каверзных вопроса с ответами

  1. Что произойдет, если блок catch (OutOfMemoryError e) успешно перехватит нехватку памяти? Ответ: Приложение продолжит работать, но с поврежденным состоянием: мониторы синхронизации разблокированы в случайных местах, внутренние пулы потоков и транзакции сломаны, а любая следующая операция (даже запись в лог) снова выбросит OutOfMemoryError. Это переводит сервис в режим «зомби». Правильное решение — завершить процесс с помощью флага -XX:+CrashOnOutOfMemoryError.

  2. В чем разница между NoClassDefFoundError и ClassNotFoundException? Ответ: ClassNotFoundException — это проверяемое исключение, возникающее при динамической рефлексивной загрузке (Class.forName(), ClassLoader.loadClass()), когда файл класса не найден. NoClassDefFoundError — это системная ошибка (Error), возникающая при попытке использовать класс, который успешно скомпилировался, но в рантайме отсутствует в classpath либо упал при выполнении статического блока инициализации <clinit>.

  3. К какой категории относится AssertionError и почему? Ответ: AssertionError является прямым наследником Error. Он выбрасывается оператором assert при нарушении внутренних инвариантов алгоритма. Авторы Java сделали его Error, чтобы он намеренно не перехватывался стандартными блоками catch (Exception e), гарантируя падение программы при нарушении постусловий и инвариантов.

  4. В чем разница между OutOfMemoryError: Java heap space и OutOfMemoryError: Metaspace? Ответ: Java heap space означает нехватку памяти в куче для создания новых объектов бизнес-логики. Metaspace означает нехватку нативной памяти операционной системы для хранения метаданных классов (пул констант, байткод методов, таблицы виртуальных методов), что обычно вызвано утечкой загрузчиков классов (ClassLoader leak) или неконтролируемой генерацией прокси.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Error — это проверяемое исключение, требующее обязательного try-catch» (Error является unchecked).
  • ❌ «Если поймать StackOverflowError, можно продолжить нормальную работу метода» (стек потока разрушен, поток следует немедленно прекратить).
  • ❌ «NoClassDefFoundError и ClassNotFoundException — это одно и то же исключение с разными именами» (один — Error линковки, второй — checked Exception рефлексии).
  • ❌ «В Java все ошибки можно и нужно лечить через catch (Error e)» (грубейший антипаттерн, приводящий к зависанию приложений в продакшене).

Связанные темы