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

Что находится в вершине иерархии исключений?

В самом верху иерархии всех объектов, которые можно выбросить и перехватить, находится класс java.lang.Throwable.

Версии по языкам: English Russian Ukrainian

Junior Level

Корень иерархии — java.lang.Throwable

В самом верху иерархии всех объектов, которые можно выбросить и перехватить, находится класс java.lang.Throwable.

Object
  └── Throwable
        ├── Error (фатальные ошибки JVM)
        │     ├── OutOfMemoryError
        │     ├── StackOverflowError
        │     └── NoClassDefFoundError
        │
        └── Exception (ошибки приложения)
              ├── RuntimeException (unchecked)
              │     ├── NullPointerException
              │     └── IllegalArgumentException
              │
              └── Checked Exceptions
                    ├── IOException
                    └── SQLException

Две основные ветви

Error — фатальные проблемы JVM:

  • OutOfMemoryError — закончилась память
  • StackOverflowError — бесконечная рекурсия
  • Ловить не нужно — система в нестабильном состоянии

Exception — ошибки приложения:

  • Можно и нужно обрабатывать
  • Делятся на checked и unchecked

Error – проблемы, которые приложение не может исправить (память, стек). Exception – проблемы, которые может. Разделение в иерархии позволяет ловить Exception, не ловя Error.

Пример

// Throwable — родитель всех исключений
try {
    // какой-то код
} catch (Throwable t) {
    // Перехватит ВСЁ, включая Error (обычно так не делают)
    System.err.println("Caught: " + t.getMessage());
}

Middle Level

Почему Throwable реализует Serializable?

Сериализация – превращение объекта в байты для передачи по сети или сохранения. Это нужно для RMI (удалённых вызовов), JMX, кеширования. Исключение должно быть сериализуемо, чтобы «путешествовать» между JVM.

Это критично для распределённых систем (RMI, JMX). Когда исключение происходит на сервере, JVM сериализует его вместе со стек-трейсом и передаёт клиенту.

Ключевые подклассы Error

  • OutOfMemoryError — разные подтипы:
    • Java heap space — закончилась куча
    • Metaspace — закончилась память под метаданные классов
    • Unable to create new native thread — ОС запретила потоки
  • ThreadDeath — выбрасывается при Thread.stop() (deprecated). Это Error, чтобы обычный catch (Exception e) не проглотил его.

  • NoClassDefFoundError — класс был при компиляции, но пропал в рантайме. Нарушена целостность среды.

Проблема проброса через ExecutorService

Когда исключение происходит внутри потока в пуле, оно перехватывается ExecutorService и сохраняется внутри Future:

Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Task failed!");
});

// Исключение "спрятано" внутри Future
try {
    future.get(); // Здесь вылетит ExecutionException
} catch (ExecutionException e) {
    System.err.println("Cause: " + e.getCause()); // RuntimeException
}
// ExecutorService перехватывает все исключения из задач и сохраняет их внутри Future.
// При вызове future.get() exception оборачивается в ExecutionException.
// Чтобы добраться до причины: e.getCause().

fillInStackTrace()

fillInStackTrace() – нативный метод (написан на C/C++, не на Java). Обходит стек вызовов на уровне ОС, создавая снимок всех кадров. Это дорого: требует перехода между Java и нативным кодом + обход всего стека.

При вызове getStackTrace() нативная структура преобразуется в массив StackTraceElement[] — это ленивая оптимизация, но заполнение нативной структуры всё равно дорогое.


Senior Level

Under the Hood: скрытое поле backtrace

Внутри Throwable есть поле private Object backtrace, заполняемое нативным методом fillInStackTrace(). Оно хранит ссылку на нативный стек вызовов в памяти JVM.

Serialization Pitfall: при сериализации нативный backtrace теряется, и JVM конвертирует его в массив Java-объектов. Это увеличивает размер пакета и нагрузку на CPU.

Отключение стек-трейса для производительности

public class FastException extends RuntimeException {
    public FastException(String message) {
        // enableSuppression = true, writableStackTrace = false
        super(message, null, true, false);
    }
}

Ускоряет создание в десятки раз — нативный обход стека не выполняется.

Immutable Exceptions

В высоконагруженных системах создают статические инстансы:

public static final MyException INVALID_DATA = 
    new MyException("Invalid data", null, true, false);

Не создаёт новые объекты, но в логах будет одинаковый стек-трейс.

Suppressed Exceptions

Введены в Java 7 для try-with-resources. Если исключение возникло в try, а второе — при закрытии ресурса в finally, первое становится основным, второе добавляется в suppressed:

try {
    resource.doWork(); // IOException #1
} finally {
    resource.close();  // IOException #2
}
// IOException #1 — основное, #2 — suppressed

Диагностика

  • Никогда не делайте catch (Throwable t) в бизнес-логике — перехватит OutOfMemoryError
  • Root Cause Analysis — используйте ExceptionUtils.getRootCause() для поиска первопричины
  • Распределённые системы — на границе сервиса извлекайте сообщение и код ошибки, передавайте только DTO

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

Обязательно знать:

  • Корень иерархии — java.lang.Throwable (не Exception)
  • Две ветви: Error (фатальные проблемы JVM) и Exception (ошибки приложения)
  • Error — OutOfMemoryError, StackOverflowError, NoClassDefFoundError — не ловить
  • Exception делится на RuntimeException (unchecked) и checked exceptions
  • fillInStackTrace() — нативный метод, обходит стек вызовов, дорогая операция
  • Suppressed exceptions — механизм Java 7 для try-with-resources (сохраняет второе исключение)
  • Отключение стек-трейсa (writableStackTrace = false) ускоряет создание в десятки раз
  • При сериализации нативный backtrace теряется — конвертируется в Java-объекты

Частые уточняющие вопросы:

  • Почему Throwable реализует Serializable? — Для RMI, JMX, кеширования — исключения «путешествуют» между JVM
  • Что будет, если перехватить OutOfMemoryError? — JVM может быть в нестабильном состоянии, следующая операция упадёт
  • Что такое suppressed exceptions? — Когда в try и close() упали два исключения, второе добавляется в suppressed-список первого
  • Почему нельзя создавать immutable исключения для всего? — Одинаковый стек-трейс затрудняет отладку

Красные флаги (НЕ говорить):

  • “Корень иерархии — Exception” — нет, Throwable
  • “Я ловлю Throwable в бизнес-логике для надёжности” — это перехватит OutOfMemoryError
  • “fillInStackTrace() — обычный Java-метод” — нет, нативный, обходит стек на уровне ОС

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

  • [[Что такое Throwable]]
  • [[В чём разница между Error и Exception]]
  • [[Что такое suppressed exceptions]]
  • [[Что такое stack trace]]
  • [[Что такое try-with-resources]]