⚡ Розділ 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 або вичерпання системних ресурсів ОС. Нештатна ситуація в логіці застосунку або тимчасовий збій оточення.
Можливість відновлення Невідновлювано. Застосунок не здатний програмно усунути причину збою. Відновлювано. Програма може перехопити помилку, застосувати fallback і продовжити роботу.
Контроль компілятора Неперевірюване (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

Одне з найпопулярніших запитань на технічних інтерв’ю з Java Core:

                            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 leak).
  • 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) або catch (Error e), застосунок продовжить висіти в пам’яті. Він перестане відповідати на вхідні запити або зависне на першому ж логуванні, але процес не впаде, через що 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-блоці породжує 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 — це перевірюваний виняток при явному динамічному пошуку класу за ім’ям у Class.forName(), тоді як NoClassDefFoundError — це фатальний Error компонування (лінковки), що виникає, коли клас був під час компіляції, але відсутній у рантаймі, або коли впав його статичний блок ініціалізації 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 означає вичерпання нативної пам’яті операційної системи для збереження метаданих класів (пул констант, байткод методів, vtable), що найчастіше викликано витоком завантажувачів класів (ClassLoader leak) або неконтрольованим створенням CGLIB/Spring-проксі.

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «Error — це перевірюваний виняток, який потребує обов’язкового блоку try-catch» (Error є неперевірюваним / unchecked).
  • ❌ «Якщо перехопити StackOverflowError, можна спокійно продовжити виконання методу» (стек потоку зруйнований, потік необхідно негайно завершити).
  • ❌ «NoClassDefFoundError і ClassNotFoundException — це одна й та сама помилка, просто з різними назвами» (один — Error лінковки, другий — checked Exception динамічного завантаження).
  • ❌ «У надійних корпоративних системах усі помилки потрібно лікувати через catch (Error e)» (грубий антипатерн, що призводить до зависання систем у продакшені).

Пов’язані теми