JavaMiddleExperience

Какие особенности runtime, type system или memory model Java реально влияют на архитектуру приложения?

JVM GC-паузы, JMM happens-before, erasure generics и Virtual Threads (Java 21) напрямую определяют выбор архитектурных паттернов — от пулов потоков до off-heap буферов.

JVM runtime и архитектурные решения

Java-приложения работают на JVM, и это напрямую влияет на архитектуру. JIT-компилятор (C1/C2 в HotSpot, Graal в GraalVM) достигает пиковой производительности через 10–30 секунд прогрева — это означает, что stateless-микросервисы с частыми перезапусками теряют на cold start. Решение: Java 21+ с Virtual Threads (Project Loom) и GraalVM Native Image для мгновенного старта.

Garbage Collector определяет характер пауз. G1GC (default с Java 9) даёт pause goals около 200 мс. ZGC и Shenandoah дают паузы менее 1 мс при heap до 16 TB — это критично для low-latency trading или real-time API. Архитектурное следствие: не держите огромные объекты в heap; используйте off-heap буферы (DirectByteBuffer) или разбивайте обработку на chunks.

// Пример off-heap буфера для I/O-intensive кода
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024 * 1024); // 64 MB вне heap
// GC не трогает этот буфер — нет пауз при чтении/записи больших файлов

Memory model (JMM) и многопоточность

Java Memory Model гарантирует happens-before отношения. Без volatile или synchronized изменения в одном потоке не гарантированно видны в другом. Это влияет на архитектуру: double-checked locking без volatile на поле сломан до Java 5. Правильный Singleton:

public class Config {
    private static volatile Config instance; // volatile обязателен

    public static Config getInstance() {
        if (instance == null) {
            synchronized (Config.class) {
                if (instance == null) {
                    instance = new Config();
                }
            }
        }
        return instance;
    }
}

Лучше использовать Initialization-on-demand holder pattern или enum singleton — они thread-safe без явной синхронизации.

Type system и стирание типов

Generics в Java реализованы через erasure: List<String> и List<Integer> в байткоде одно и то же — List. Это ограничивает рефлексию и влияет на сериализацию. Jackson использует TypeReference для обхода:

// Неверно — тип стёрт, Jackson вернёт List<LinkedHashMap>
List<User> users = objectMapper.readValue(json, List.class);

// Верно — TypeReference сохраняет параметрический тип
List<User> users = objectMapper.readValue(json,
    new TypeReference<List<User>>() {});

Sealed classes (Java 17+) и records (Java 16+) дают алгебраические типы данных, что упрощает моделирование предметной области без null-проверок:

sealed interface PaymentResult permits Success, Failure, Pending {}
record Success(String transactionId) implements PaymentResult {}
record Failure(String reason) implements PaymentResult {}

// Pattern matching в switch (Java 21+)
String message = switch (result) {
    case Success s -> "OK: " + s.transactionId();
    case Failure f -> "Error: " + f.reason();
    case Pending p -> "Wait";
};

Virtual Threads (Java 21)

Project Loom добавил виртуальные потоки — легковесные потоки, которые не блокируют OS-поток при I/O. Это меняет архитектуру: больше не нужен реактивный стек (Reactor, RxJava) для масштабирования I/O-heavy сервисов. Spring Boot 3.2+ включает Virtual Threads одним свойством:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

Подводные камни

  • PermGen/Metaspace: до Java 8 утечки ClassLoader приводили к OutOfMemoryError в PermGen. Сейчас Metaspace в native memory, но динамическая генерация классов (Cglib, Groovy) всё ещё может его исчерпать.
  • String interning: String.intern() кладёт строку в String Pool. Злоупотребление — утечка памяти в Metaspace.
  • Finalizers устарели: System.gc() не гарантирует вызов finalize(). Используйте Cleaner API или try-with-resources.
  • ThreadLocal утечки в пулах потоков: если не очищать ThreadLocal в конце задачи, данные предыдущей задачи попадают в следующую.
  • JIT warmup в тестах: microbenchmark без JMH (Java Microbenchmark Harness) измеряет интерпретируемый код, а не оптимизированный JIT.
  • Checked exceptions ломают функциональные интерфейсы: лямбды не могут бросать checked exceptions — нужны обёртки или sneaky throws через Lombok.
  • equals/hashCode и коллекции: если переопределить equals без hashCode (или наоборот), объекты теряются в HashMap.
  • Integer кэш: Integer.valueOf(127) == Integer.valueOf(127) — true (кэш -128..127), но Integer.valueOf(200) == Integer.valueOf(200) — false. Используйте .equals().

What hurts your answer

  • Знать термины Java, но не понимать связи между абстракциями
  • Объяснять поведение через отдельные примеры вместо причинной модели
  • Не связывать mental model с диагностикой ошибок

What they're listening for

  • Понимает ключевые абстракции Java
  • Может предсказывать поведение системы через mental model
  • Связывает модель с debugging и production decisions

Related topics