⚡ Розділ 7 · Питання #5

Що знаходиться на вершині ієрархії винятків

На самій вершині ієрархії всіх винятків та системних помилок у мові Java знаходиться клас java.lang.Throwable (який успадковується безпосередньо від java.lang.Object).


🟢 Junior Level

На самій вершині ієрархії всіх винятків та системних помилок у мові Java знаходиться клас java.lang.Throwable (який успадковується безпосередньо від java.lang.Object).

Тільки об’єкти класу Throwable або його нащадків можуть бути:

  1. Викинені за допомогою ключового слова throw.
  2. Оголошені в сигнатурі методу за допомогою ключового слова throws.
  3. Перехоплені в секції catch (...) блоку try-catch.

Спроба написати throw new String("Error"); або catch (Object obj) призведе до помилки компіляції.

Деревоподібна структура ієрархії

                           java.lang.Object
                                  │
                         java.lang.Throwable
                                  │
                 ┌────────────────┴────────────────┐
                 ▼                                 ▼
         java.lang.Error                 java.lang.Exception
      (Системні аварії JVM)           (Помилки логіки та середовища)
                 │                                 │
     ┌───────────┴───────────┐         ┌───────────┴───────────┐
     ▼                       ▼         ▼                       ▼
OutOfMemoryError   StackOverflowError Checked Exceptions  RuntimeException
                                      (IOException,       (Unchecked: NPE,
                                       SQLException)       IllegalArgument)

Дві головні гілки

  1. java.lang.Error: Сигналізує про критичні апаратні або системні збої віртуальної машини JVM (вичерпання фізичної пам’яті, переповнення стека потоку, пошкодження байткоду). Звичайний прикладний код не повинен намагатися перехоплювати Error.
  2. java.lang.Exception: Сигналізує про помилки рівня застосунку та інфраструктури (не знайдено файл, відпала мережа, передано невалідні дані). Має оброблятися програмно.

🟡 Middle Level

Внутрішня будова класу Throwable

Клас Throwable інкапсулює 5 ключових полів, що визначають стан будь-якої помилки:

public class Throwable implements Serializable {
    private String detailMessage;             // Текстовий опис помилки
    private Throwable cause = this;          // Першопричина (вкладений виняток)
    private StackTraceElement[] stackTrace;  // Масив фреймів стека викликів
    private List<Throwable> suppressedExceptions; // Пригнічені винятки (Java 7+)
    private transient Object backtrace;      // Нативний вказівник на стек JVM
}
  1. detailMessage: Повідомлення, що передається в конструктор і повертається методом getMessage().
  2. cause (Chained Exceptions): Посилання на першопричину збою (ініціалізується в конструкторі або через метод initCause(Throwable)). Дозволяє досліджувати ланцюжок причин за допомогою getCause().
  3. backtrace: Приховане transient поле, що заповнюється нативним методом fillInStackTrace() під час створення об’єкта. Зберігає знімок кадрів виконання потоку на рівні ядра JVM.
  4. suppressedExceptions: Список винятків, пригнічених у конструкції try-with-resources (доступний через getSuppressed()).

Чому Throwable реалізує java.io.Serializable?

Історично за часів створення Java (1995–1997 рр.) ключовою розподіленою технологією був протокол RMI (Remote Method Invocation) та специфікація EJB. Якщо метод віддаленого сервера завершувався помилкою, JVM зобов’язана була серіалізувати об’єкт винятку мережею та передати його клієнту.

Сучасний підводний камінь: Під час серіалізації нативний вказівник backtrace недоступний. JVM змушена «матеріалізувати» нативну структуру у величезний масив важковагових Java-об’єктів StackTraceElement[], що призводить до значного роздування мережевого трафіку та витрат пам’яті.


🔴 Senior Level

StackWalker API (Java 9+): Сучасна альтернатива Throwable

До Java 9 єдиним способом програмного аналізу стека викликів було створення фіктивного винятку:

StackTraceElement[] frames = new Throwable().getStackTrace();

Цей підхід є вкрай неефективним: JVM робить повну зупинку потоку, збирає абсолютно всі фрейми до самої основи (до main або потоку Tomcat) та виділяє пам’ять під весь масив.

Починаючи з Java 9, стандартом аналізу стека став java.lang.StackWalker:

  • Підтримує ліниве (lazy) вичитування: фрейми парсяться тільки доти, доки ви читаєте потік Stream<StackFrame>.
  • Дозволяє відфільтровувати внутрішні класи рефлексії та Spring CGLIB проксі без виділення зайвої пам’яті в купі.
package com.example.diagnostic;

import java.lang.StackWalker.StackFrame;
import java.util.List;

public class StackInspector {

    private static final StackWalker WALKER = 
            StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);

    // Витягує ім'я безпосереднього викликаючого методу за O(1) без створення Exception
    public static String getImmediateCaller() {
        return WALKER.walk(frames -> frames
                .skip(1) // Пропускаємо сам метод getImmediateCaller
                .findFirst()
                .map(StackFrame::getMethodName)
                .orElse("UNKNOWN"));
    }

    // Пошук першого класу поза поточним пакетом
    public static List<String> getExternalCallers() {
        return WALKER.walk(frames -> frames
                .map(StackFrame::getClassName)
                .filter(className -> !className.startsWith("com.example.diagnostic"))
                .limit(5)
                .toList());
    }
}

Конструктор Java 7+ із прапорцями пригнічення та стека

У класі Throwable присутній захищений конструктор:

protected Throwable(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace)
  • enableSuppression = false: Забороняє накопичення пригнічених винятків (економить пам’ять, getSuppressed() поверне порожній масив).
  • writableStackTrace = false: Повністю блокує виклик fillInStackTrace(). Поле backtrace залишається null, стек-трейс не формується. Це прискорює створення екземпляра винятку в десятки разів.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

У вершині ієрархії винятків Java знаходиться клас java.lang.Throwable. Тільки його нащадки можуть передаватися в оператори throw, throws та catch.

Throwable поділяється на дві фундаментальні гілки:

  1. java.lang.Error — неусувні системні відмови віртуальної машини JVM (OutOfMemoryError, StackOverflowError), які прикладний код не повинен перехоплювати.
  2. java.lang.Exception — прикладні помилки (поділяються на checked та unchecked RuntimeException).

Всередині Throwable містить message, першопричину cause, масив suppressedExceptions та приховане нативне поле backtrace, що заповнюється методом fillInStackTrace(). Починаючи з Java 9, для високопродуктивного аналізу стека викликів замість new Throwable().getStackTrace() використовується потоковий StackWalker.

4 каверзних питання з відповідями

  1. Чи можна викинути екземпляр класу Object, написавши throw new Object();? Відповідь: Ні, код не скомпілюється. Специфікація Java (JLS §14.18) вимагає, щоб операнд оператора throw мав тип, привідний до java.lang.Throwable.

  2. Навіщо в Java 7 до класу Throwable додали механізм Suppressed Exceptions? Відповідь: Механізм додано для підтримки конструкції try-with-resources. Якщо виняток викинуто з тіла блоку try, а під час закриття ресурсу в close() виникає другий виняток, перший виняток викидається як основний, а другий прикріплюється до нього викликом addSuppressed(), запобігаючи втраті інформації про збій.

  3. Чому StackWalker працює швидше, ніж Thread.currentThread().getStackTrace() або new Throwable().getStackTrace()? Відповідь: Старий підхід монолітно збирає повний стек викликів і матеріалізує всі елементи в масив StackTraceElement[]. StackWalker працює ліниво (lazily) через Stream<StackFrame>, дозволяючи перервати читання стека на потрібній глибині (наприклад, взяти тільки 1-й фрейм викликаючого) без розбору решти 50 фреймів.

  4. Чому перехоплення catch (Throwable t) у сервісі призводить до зомбі-процесів під час витоку пам’яті? Відповідь: Тому що блок перехопить java.lang.OutOfMemoryError. Пам’ять купи не звільниться, але процес продовжить існувати з порушеними інваріантами та завислими блокуваннями, не відповідаючи на запити й не падаючи, що перешкоджає оркестратору (Kubernetes) перезапустити дефектний под.

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

  • ❌ «У вершині ієрархії винятків знаходиться клас Exception» (у вершині знаходиться Throwable, а над ним лише Object).
  • ❌ «Error успадковується від Exception» (Error та Exception — паралельні незалежні гілки, що успадковуються від Throwable).
  • ❌ «Щоб дізнатися, хто викликав мій метод, треба створити new Throwable() і подивитися нульовий елемент масиву» (з Java 9 для цього існує легковажний StackWalker).
  • ❌ «Throwable корисно перехоплювати в кожному методі, це гарантує 100% аптайм» (перехоплення Error ламає стабільність JVM).

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