Java Java: обзор языка, платформы JVM и дорожная карта курса
0%

Java: обзор языка, платформы JVM и дорожная карта курса

Java: обзор языка, платформы JVM и дорожная карта курса

Java — редкий случай, когда язык проще всего объяснить через задачу, которую он решал. В начале 1990-х Sun пыталась писать софт для бытовой электроники: приставки, пульты, терминалы. Железо у каждого производителя своё, компилятор C++ у каждого свой, размер int зависит от платформы, а половина багов — обращение к освобождённой памяти. Команда Джеймса Гослинга сделала ход, который в тот момент выглядел расточительно: зафиксировать не железо, а виртуальную машину. Программа компилируется не в машинный код, а в байткод абстрактного процессора; этот процессор эмулируется на любой платформе; память освобождает сборщик мусора; семантика описана в спецификации до последнего бита.

Ставка сыграла не в приставках, а в серверах. Тридцать лет спустя на JVM работают Kafka, Spark, Flink, Cassandra, Elasticsearch, большая часть банковских бэкендов и половина российского энтерпрайза. Поэтому первое, что нужно понять про Java: вы учите не язык, вы въезжаете в платформу. Язык — это верхушка; ценность внизу.

Три разные вещи, которые называют словом «Java»

Путаница здесь стоит новичкам недель, поэтому разведём термины сразу.

  1. Язык Java — синтаксис и статическая семантика, описанные в Java Language Specification. Это то, что проверяет компилятор javac.
  2. Платформа JVM — формат файла .class, набор байткодовых инструкций и правила исполнения, описанные в Java Virtual Machine Specification. JVM ничего не знает про язык Java: она исполняет байткод. Поэтому на ней живут Kotlin, Scala, Clojure, Groovy — и их классы свободно вызывают друг друга.
  3. JDK — конкретная реализация: компилятор, JVM (обычно HotSpot), стандартная библиотека, инструменты (javap, jcmd, jfr, jlink). Реализаций много: Eclipse Temurin, Amazon Corretto, Azul Zulu, BellSoft Liberica, Oracle JDK, GraalVM, Red Hat build. Все они собраны из одного исходника OpenJDK и проходят один и тот же набор тестов совместимости (TCK). Практический вывод: выбор дистрибутива — вопрос лицензии и поддержки, а не поведения кода.

Отдельного JRE («только для запуска») с Java 11 не существует — минимальный рантайм под своё приложение вы собираете сами через jlink. Об этом подробно в статье про деплой и наблюдаемость.

Путь от исходника до машинного кода

Этот конвейер стоит один раз рассмотреть целиком: почти всё «странное» поведение Java — время прогрева, внезапное ускорение под нагрузкой, ClassNotFoundException в рантайме — объясняется именно им.

Разберём шаги, потому что каждый даёт практическое следствие.

Компиляция. javac почти не оптимизирует. Он проверяет типы, разворачивает синтаксический сахар (for-each, автобоксинг, строковую конкатенацию, лямбды) и пишет байткод. Отсюда важное: оптимизирует не компилятор, а рантайм. Читать «сгенерированный ассемблер» после javac бесполезно — там честный стековый байткод.

Загрузка классов. Классы подгружаются лениво, в момент первого использования, иерархией загрузчиков. Отсюда весь класс ошибок, невозможных в C или Go: NoClassDefFoundError, ClassNotFoundException, конфликт версий одной библиотеки в двух местах classpath. Отсюда же вся мощь Spring, Hibernate и агентов профилирования: класс можно найти, сгенерировать и переписать во время работы.

Верификация. Прежде чем исполнить, JVM доказывает, что байткод типобезопасен: не переполнит стек операндов, не приведёт Object к String без проверки. Это фундамент модели безопасности и причина, по которой «испорченный .class» не уронит процесс.

Интерпретация → JIT. Сначала байткод интерпретируется, а JVM считает вызовы и ветвления. Когда метод становится «горячим», включается многоуровневая компиляция: C1 даёт быстрый код с профилированием, C2 — агрессивно оптимизированный. C2 умеет то, чего статический компилятор не может в принципе: инлайнить виртуальные вызовы по факту, увидев, что за интерфейсом PaymentService в этой сборке всегда стоит один класс. Если предположение нарушится (загрузился второй класс), произойдёт деоптимизация: код выбрасывается, исполнение возвращается в интерпретатор. Отсюда знаменитое «Java медленно стартует, но быстро работает». Механику JIT-компиляции подробно разбирает трек компиляторов — JIT-компиляция.

Первая программа: сколько её осталось от учебника 2005 года

Каноническая версия, которую все помнят:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Привет, JVM!");
    }
}

Современная (JEP 512, финализирован в Java 25) — файл Hello.java целиком:

void main() {
    IO.println("Привет, JVM!");   // компактный исходник: без класса, без static, без System.out
}

Запускается это в обоих случаях одинаково — прямо из исходника, без промежуточной компиляции (JEP 330, с Java 11):

java Hello.java
# Привет, JVM!

Мелочь, но показательная: церемонию из Java убирают, а не добавляют. Пять слов public static void main(String[]), которые тридцать лет были первым барьером для новичка, наконец необязательны.

Посмотрим, во что превращается код. Инструмент javap — самый недооценённый в JDK:

String greet(String name) {
    return "Привет, " + name;
}

int sum(int a, int b) {
    return a + b;
}
javac Greeter.java && javap -c -p Greeter.class
  java.lang.String greet(java.lang.String);
    Code:
       0: aload_1
       1: invokedynamic #7,  0  // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
       6: areturn

  int sum(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn

Здесь видно две вещи. Первая: JVM — стековая машина, у неё нет регистров, операнды кладутся на стек (iload), инструкция их снимает (iadd), результат кладёт обратно. Вторая: конкатенация строк с Java 9 компилируется не в StringBuilder, а в invokedynamic — рантайм сам решает, какую стратегию склейки применить. Это тот же механизм, на котором работают лямбды: javac не создаёт анонимный класс, он оставляет «заявку», которую LambdaMetafactory исполняет при первом вызове.

Где живут объекты

Второй фундамент после байткода — память. Понимание раскладки экономит потом дни на разборе OutOfMemoryError и странного RSS в Kubernetes.

Карта памяти процесса JVM: куча с поколениями, Metaspace, Code Cache, стеки потоков и native-память

Ключевые следствия из картинки:

  • Стек хранит ссылку, куча — объект. Все объекты Java живут в куче, локальные переменные примитивных типов — на стеке. Аллокация в куче почти бесплатна: это сдвиг указателя внутри TLAB (thread-local allocation buffer), персонального куска Eden у каждого потока. Дорого не создать объект, а пережить его.
  • -Xmx — это не вся память процесса. RSS складывается из кучи, Metaspace, Code Cache, стеков потоков и off-heap-буферов. Классическая продовая ошибка: лимит пода 1 ГБ, -Xmx1g, и под убивает OOM Killer — потому что о трёх остальных областях никто не подумал. Разбираем в JVM изнутри.
  • Утечка в Java — это не «забыли free», а «объект всё ещё достижим, хотя больше не нужен». Классика: статический Map, растущий кэш без вытеснения, ThreadLocal в пуле потоков, слушатель, которого не отписали.

Жизненный цикл объекта и сборка мусора

Вся поколенческая сборка держится на одной эмпирической гипотезе: подавляющее большинство объектов умирает молодыми. Ответ на HTTP-запрос, DTO, промежуточный список в стриме живут миллисекунды. Значит, дёшево обходить только «молодую» область и копировать оттуда редких выживших, вместо того чтобы каждый раз обходить всю кучу.

Обратите внимание на асимметрию: мёртвые объекты в молодом поколении не стоят ничего — их не «удаляют», их просто не копируют, а Eden затирается целиком. Стоимость молодой сборки пропорциональна объёму выживших, а не объёму мусора. Отсюда контринтуитивный вывод: сделать приложение быстрее часто помогает не «меньше аллоцировать», а «аллоцировать так, чтобы объекты умирали внутри одного запроса и не переползали в Old».

Сборщиков в JDK несколько, и выбор — это осознанный компромисс между пропускной способностью, паузами и накладными расходами:

Сборщик Когда брать Цена
Serial контейнер с 1 vCPU, короткоживущие джобы, CLI останавливает мир целиком
Parallel пакетная обработка, важна пропускная способность паузы в сотни мс
G1 (по умолчанию) 90 % серверных приложений паузы десятки мс, настраивается MaxGCPauseMillis
ZGC (поколенческий) большие кучи, жёсткие требования к латентности паузы < 1 мс, +10–15 % CPU и памяти
Shenandoah то же, альтернативная реализация аналогично
Epsilon бенчмарки и тесты: «не собирать вообще» падает по OOM, и это фича

Общая теория сборки мусора — в статье Сборка мусора; конкретные флаги, логи и настройка — в JVM изнутри и Производительности.

Цена объекта: почему «всё есть объект» иногда больно

Java не даёт объявить свой тип-значение (пока — проект Valhalla в работе). У каждого объекта есть заголовок, а у каждой коллекции — косвенность. На горячем пути это видно.

Раскладка объекта в куче: заголовок, поля, выравнивание; сравнение int-массива и списка боксированных Integer

Практические следствия, о которых стоит помнить с первого дня:

  • List<Integer> из миллиона элементов — это ~20 МБ и миллион промахов кэша; int[] — 4 МБ подряд. В горячем цикле разница десятикратная.
  • Пустой HashMap уже стоит под сотню байт; сотня тысяч мелких мап — заметная часть кучи.
  • Заголовок в 12 байт — причина, по которой «микрооптимизация полей» бессмысленна, а вот выбор между объектом и примитивом осмыслен.
  • Мерить это нужно инструментом, а не глазами: JOL показывает точную раскладку, JFR — реальный профиль аллокаций. Тема раскрывается в Производительности и перекликается с треком Производительность: память.

Как Java менялась и почему учебники устарели

Если ваше представление о Java сформировано книгой десятилетней давности — оно неверно примерно наполовину. Ключевой перелом произошёл дважды: в 8-й версии (лямбды и стримы) и в 21-й (записи, sealed-типы, сопоставление с образцом, виртуальные потоки).

С 2017 года релизы выходят каждые полгода, LTS — раз в два года (11, 17, 21, 25). Правило для продакшена простое: сидите на актуальной LTS, обновляйтесь на следующую в течение её жизненного цикла. Обратная совместимость в Java почти религиозна: код, написанный в 2005-м, как правило компилируется и работает сегодня. Это одновременно главное преимущество платформы и источник её многословности — выкинуть неудачное решение нельзя почти никогда.

Моделирование данных: records и sealed вместо мешка геттеров

Двадцать лет каноничная Java-модель выглядела как класс на сто строк с полями, конструктором, геттерами, equals, hashCode, toString — и Lombok, чтобы это спрятать. Сегодня это одна строка, а иерархия закрывается sealed, что делает switch исчерпывающим:

// sealed фиксирует полный список наследников — компилятор знает их все
public sealed interface Payment permits Card, Cash, Transfer {}

// record: неизменяемые поля, конструктор, equals/hashCode/toString — сгенерированы
public record Card(String maskedPan, long amountMinor) implements Payment {}
public record Cash(long amountMinor) implements Payment {}
public record Transfer(String iban, long amountMinor, boolean urgent) implements Payment {}

static String describe(Payment p) {
    // default не нужен: компилятор доказал, что варианты покрыты все
    return switch (p) {
        case Card c when c.amountMinor() > 100_000 -> "карта, крупный платёж " + c.maskedPan();
        case Card c                                -> "карта " + c.maskedPan();
        case Cash _                                -> "наличные";               // _ — безымянный шаблон, Java 22+
        case Transfer(String iban, var amt, true)  -> "срочный перевод " + amt + " на " + iban;
        case Transfer t                            -> "перевод на " + t.iban();
    };
}

Последняя ветка использует деконструирующий шаблон: Transfer(String iban, var amt, true) одновременно проверяет тип, сопоставляет литерал true в третьей позиции и распаковывает поля в переменные. Это уже не ООП-полиморфизм — это алгебраические типы данных, приехавшие в Java из функциональных языков (см. АТД и сопоставление с образцом).

Ключевая ценность здесь не в краткости, а в том, что добавление нового варианта ломает компиляцию во всех местах, где вы забыли его обработать. Ровно это отличает надёжную доменную модель от иерархии с default: throw new IllegalStateException(). Подробно — в ООП в Java.

Виртуальные потоки: главное изменение в модели исполнения за 20 лет

До Java 21 у серверного разработчика был неприятный выбор. Либо «поток на запрос» — просто, читаемо, отлаживаемо, но платформенный поток стоит мегабайт стека и переключение в ядре, и десять тысяч одновременных соединений вас убьют. Либо реактивщина (CompletableFuture, Reactor, RxJava) — держит нагрузку, но код выворачивается наизнанку, стектрейсы перестают что-либо значить, а отладчик показывает пустоту.

Виртуальные потоки (JEP 444) убирают выбор. Это потоки, которыми управляет JVM, а не ОС: их стек — обычная структура в куче, растущая по требованию, а на блокирующем вызове поток отцепляется от несущего платформенного потока, освобождая его.

На практике это выглядит скучно — и в этом весь смысл:

// Десять тысяч одновременных задач. На платформенных потоках это ~10 ГБ стеков;
// на виртуальных — десятки мегабайт в куче.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        int id = i;
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1)); // блокирует задачу, но не поток ОС
            return id;
        });
    }
} // close() дожидается завершения всех задач — try-with-resources как структурная конкурентность

Сравните с моделью C#: там асинхронность потребовала «окрасить» функции в async/await и продублировать половину библиотек. Java выбрала другой путь — сохранить блокирующий стиль и изменить рантайм. Цена: старый код с synchronized в горячих местах может «пришпилить» (pin) виртуальный поток к несущему, а пулы потоков, которые всю жизнь были оптимизацией, внезапно стали антипаттерном. Всё это — в Конкурентности.

Грабли, на которые наступают именно в Java

Список не для заучивания — просто узнайте эти ситуации в лицо, чтобы потом не терять на них часы.

Автобоксинг и кэш Integer. Классика собеседований, которая иногда прорывается в прод:

Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);      // true  — оба из кэша Integer для -128..127
System.out.println(c == d);      // false — два разных объекта
System.out.println(c.equals(d)); // true  — сравнивать боксы надо только так

Хуже другое: Long total = 0L; for (...) total += x; создаёт объект на каждой итерации и работает в разы медленнее, чем long. Ещё хуже — Map<String, Integer> и map.get(k) == 0, которое ломается на значениях больше 127. И совсем плохо — int x = map.get(k), когда ключа нет: тихий NullPointerException на распаковке null.

Сравнение строк через ==. Литералы интернируются, поэтому == иногда «работает» и создаёт ложное ощущение правильности:

String s1 = "java";
String s2 = "ja" + "va";           // склеено компилятором → та же константа
String s3 = new String("java");
System.out.println(s1 == s2);      // true
System.out.println(s1 == s3);      // false
System.out.println(s1.equals(s3)); // true — единственный правильный способ

Арифметика. int переполняется молча, а double не умеет считать деньги:

System.out.println(Integer.MAX_VALUE + 1);   // -2147483648, без единого предупреждения
System.out.println(0.1 + 0.2);               // 0.30000000000000004
System.out.println(new BigDecimal(0.1));     // 0.1000000000000000055511151231257827...
System.out.println(BigDecimal.valueOf(0.1)); // 0.1 — valueOf идёт через строку

В деньгах — либо BigDecimal.valueOf / new BigDecimal("0.1"), либо (что чаще правильнее) целые копейки в long. Math.addExact бросит исключение вместо тихого переполнения.

Перегрузка, которая маскируется под индекс. Одна из самых злых ловушек стандартной библиотеки:

List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);                    // remove(int index) → удалит 20!
list.remove(Integer.valueOf(30));  // remove(Object)    → удалит 30

Три разных «неизменяемых» списка. Arrays.asList — фиксированного размера, но элементы менять можно, и он смотрит на исходный массив. Collections.unmodifiableList — обёртка, которая меняется, если поменять оригинал. List.of — по-настоящему неизменяемый, но бросает NullPointerException на null-элементе. Подробно — в Коллекциях и дженериках.

Стирание типов. Дженерики в Java существуют только на этапе компиляции:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true — в рантайме оба просто ArrayList

Отсюда: нельзя new T[], нельзя instanceof List<String>, нельзя перегрузить метод по List<String> и List<Integer>. В C#/.NET дженерики реифицированы, и это одно из самых заметных различий двух похожих платформ.

Изменяемый ключ в HashMap. Положили объект ключом, потом поменяли поле, участвующее в hashCode — и объект «потерялся» в мапе навсегда: он лежит в корзине по старому хэшу. Ключи должны быть неизменяемыми, поэтому record — почти идеальный ключ.

NullPointerException как способ жизни. Java не различает на уровне типов «может быть null» и «не может» (в отличие от C# с nullable reference types или Kotlin). Спасают дисциплина, Optional в возвращаемых значениях (но не в полях и не в параметрах!) и подробные сообщения JVM начиная с Java 14:

Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.length()" because the return value of "Order.comment()" is null

Проглоченные исключения. catch (Exception e) {} — самый дорогой три-символьный блок в индустрии. Обратная сторона: checked-исключения не дружат с лямбдами, что толкает людей оборачивать всё в RuntimeException без контекста. Как жить правильно — в Идиоматике и обработке ошибок.

Потокобезопасность по невнимательности. SimpleDateFormat не потокобезопасен (используйте java.time.format.DateTimeFormatter), HashMap под конкурентной записью теряет данные, double-checked locking без volatile не работает, а synchronized внутри виртуального потока может его пришпилить.

Контейнеры. До JDK 10 JVM не видела cgroup-лимиты и определяла память по хосту. Сейчас UseContainerSupport включён по умолчанию, но по-прежнему стоит явно задавать -XX:MaxRAMPercentage вместо -Xmx, чтобы образ переезжал между окружениями без правок.

Честно: где Java выигрывает, а где нет

Обзорная статья обязана быть честной, иначе она реклама.

Java уверенно выигрывает там, где:

  • процесс живёт долго — сервисы, брокеры, ETL. Прогрев JIT амортизируется, и в стационарном режиме код по скорости сопоставим с C++ на типичной бизнес-логике;
  • нужны большие данные: Kafka, Spark, Flink, Cassandra, Elasticsearch/Lucene, Hadoop, Trino — всё это JVM. Войти в data-инфраструктуру, не зная JVM, практически нельзя (см. трек Data Engineering);
  • важна долгая жизнь кодовой базы: обратная совместимость беспрецедентна, а стоимость найма разработчиков предсказуема;
  • нужна наблюдаемость в проде: JFR, JMX, heap dump, async-profiler, jcmd дают уровень интроспекции живого процесса, которого нет почти нигде;
  • задача — интеграция и энтерпрайз: транзакции, очереди, SOAP/REST, ORM, batch — здесь экосистема просто зрелее всех остальных.

Java проигрывает там, где:

  • важен холодный старт и маленький footprint: CLI-утилиты, короткие Lambda-функции, скрипты. Стандартный старт JVM — сотни миллисекунд, базовый RSS — десятки мегабайт. Лечится GraalVM native-image, AppCDS, проектом Leyden и CRaC, но это отдельная работа, а не «из коробки»;
  • нужен полный контроль над памятью и предсказуемость до микросекунды: ядра ОС, драйверы, hard real-time. Здесь C, C++, Rust (трек Системное программирование);
  • задача — исследования в ML/DS: экосистема Python вне конкуренции, а DJL и Tribuo нишевые;
  • нужен фронтенд в браузере — это территория TypeScript (трек TypeScript);
  • команда мелкая, а домен простой: Java требует больше церемонии, чем Go или Python. Иногда честный ответ — «нам не нужна JVM».

Куда Java не стоит тащить принципиально. В короткоживущие процессы, вызываемые тысячи раз в секунду (каждый запуск — заново прогрев). В embedded с несколькими мегабайтами памяти. В задачи, где вся логика — обвязка вокруг вызова внешнего инструмента: там скрипт честнее.

Соседство с C#/.NET: близко, но не одно и то же

На портале есть трек C#/.NET — ниша та же: управляемый рантайм, JIT, GC, статическая типизация, корпоративный бэкенд. Различия при этом глубже, чем синтаксис.

Аспект Java C#/.NET
Дженерики стирание типов, List<T> в рантайме — просто List реифицированы, typeof(List<int>) существует
Типы-значения пока нет своих (проект Valhalla) struct, record struct, Span<T>
Асинхронность виртуальные потоки: блокирующий код без окраски функций async/await: явная окраска, две версии API
Работа с данными Stream API, коллекторы LINQ + деревья выражений (провайдеры к БД)
Null-безопасность нет на уровне типов, спасает дисциплина и Optional nullable reference types в компиляторе
Управление платформой OpenJDK, много независимых вендоров преимущественно Microsoft
Пакеты Maven Central, координаты groupId:artifactId:version NuGet
AOT внешний GraalVM native-image встроен в SDK
Десктоп/игры слабое место WPF, MAUI, Unity
Big data доминирует почти отсутствует

Практический вывод: если вы пришли из C#, синтаксис вы прочитаете сразу, а спотыкаться будете на стирании типов, отсутствии структур, модели конкурентности и на культуре сборки (Maven и Gradle устроены совсем не как dotnet build).

Экосистема одним взглядом

Обратите внимание на нижнюю ветку: знание JVM переносимо. Освоив память, GC и байткод, вы получаете доступ к Kotlin и Scala почти бесплатно — меняется синтаксис, но не модель исполнения.

Дорожная карта курса

Курс устроен как лестница: от установки JDK до профилирования живого сервиса под нагрузкой. Каждая статья опирается на предыдущие, поэтому лучше идти по порядку.

Фундамент

  1. Установка и инструментарий — выбор дистрибутива JDK, переключение версий через SDKMAN, Maven против Gradle, структура проекта, IDE, форматтеры.
  2. Основы Java — примитивы и ссылки, копирование по значению ссылки, строки и их неизменяемость, var, управление потоком, массивы.
  3. ООП в Java — классы, интерфейсы с default-методами, композиция против наследования, record, sealed, сопоставление с образцом, equals/hashCode.
  4. Коллекции и дженерики — внутреннее устройство ArrayList и HashMap, выбор структуры под задачу, стирание типов, ? extends / ? super и правило PECS.

Идиоматика

  1. Идиоматика и обработка ошибок — checked против unchecked, try-with-resources, честное применение Optional, неизменяемость по умолчанию.
  2. Stream API и функциональный стиль — лямбды и ссылки на методы, ленивость, коллекторы, когда стрим уместен и когда цикл честнее.
  3. Конкурентность — модель памяти Java, synchronized и volatile, executors, CompletableFuture, виртуальные потоки и структурная конкурентность.

Платформа

  1. JVM изнутри — байткод и javap, загрузчики классов, области памяти, поколенческий GC, выбор и настройка сборщика, многоуровневый JIT.

Инженерная практика

  1. Тестирование — JUnit 5, параметризованные тесты, AssertJ, Mockito без злоупотреблений, Testcontainers и честные интеграционные тесты.
  2. Spring и Spring Boot — внедрение зависимостей, контекст, автоконфигурация, web-слой, что именно делает стартер под капотом.
  3. Работа с данными — JDBC, пул соединений, JPA и Hibernate, транзакции и их границы, проблема N+1, ленивые загрузки и миграции.
  4. Архитектура прод-приложений — слои и модули, конфигурация и профили, идемпотентность, таймауты, retry, circuit breaker.

Эксплуатация

  1. Производительность — методика измерения, JFR и async-profiler, микробенчмарки на JMH и их ловушки, настройка GC, поиск утечек.
  2. Деплой и наблюдаемость — fat jar против слоёных образов, jlink и native-image, метрики Micrometer, логи, трейсинг OpenTelemetry.
  3. Java в SDLC — CI/CD, управление зависимостями и уязвимостями, статический анализ, обновление версий и курированный список ресурсов.

Как проходить этот курс

  • Держите открытый scratch-проект. Всё, что видите в статьях, набирайте руками и запускайте. Для одиночных примеров хватит jshell (REPL, встроен в JDK с версии 9) или java Файл.java.
  • Смотрите под капот при каждом удобном случае. Не понимаете, во что превратилась конструкция — javap -c. Не понимаете, куда ушла память — heap dump и JFR. Java уникально прозрачна в рантайме, и это её главный обучающий бонус.
  • Читайте спецификацию, а не пересказы. JLS и JVMS написаны сухо, но однозначно. Один раз найдя ответ там, вы перестанете спорить о поведении языка.
  • Не верьте бенчмаркам без JMH. Из-за JIT наивный замер System.nanoTime() вокруг цикла в Java врёт систематически: код успевает соптимизироваться, а иногда и вовсе исчезнуть.
  • Сначала LTS, потом новинки. Учитесь на Java 21 или 25: там уже есть всё современное, и это то, что вы встретите в вакансиях.
  • Не пропускайте главы про JVM и конкурентность, даже если пришли опытным разработчиком из другого языка. Именно там прячется специфика, на которой спотыкаются сеньоры.

Сквозной пример курса

Чтобы фичи не висели в воздухе, через статьи проходит один пример — сервис платежей (payments). Мы будем возвращаться к нему в разных разрезах: смоделируем домен записями и sealed-типами, разложим ошибки на ожидаемые и исключительные, соберём отчёты стримами, распараллелим обращения к внешним провайдерам виртуальными потоками, покроем тестами с реальной базой в Testcontainers, разложим по слоям, положим за Spring Boot, а затем профилируем и выкатим в контейнере с метриками и трейсами.

Мини-итог

  • Java — это в первую очередь платформа: спецификация языка, спецификация JVM и экосистема. Язык можно выучить за неделю, платформу изучают годами.
  • Компилятор почти не оптимизирует; всю тяжёлую работу делает рантайм — многоуровневый JIT, инлайнинг по профилю, деоптимизация. Отсюда прогрев и высокая пиковая скорость.
  • Память делится на кучу с поколениями, Metaspace, Code Cache, стеки и off-heap. -Xmx — не весь процесс, и это самая частая причина падений в Kubernetes.
  • Утечка в Java — это достижимый, но ненужный объект. GC избавляет от free, но не от ответственности за граф ссылок.
  • Современная Java (17/21/25) — это записи, sealed-типы, сопоставление с образцом и виртуальные потоки. Учебники десятилетней давности описывают другой язык.
  • Ниша честная: долгоживущие серверные процессы, интеграции и большие данные. Для CLI, embedded и ML есть инструменты лучше.

Источники

  • Java Language Specification и JVM Specification — первоисточник.
  • docs.oracle.com/en/java — API и руководства JDK.
  • openjdk.org/jeps/0 — индекс JEP: почему каждая фича появилась.
  • inside.java и dev.java — материалы команды OpenJDK.
  • Joshua Bloch, «Effective Java», 3-е издание — канон идиоматики.
  • Brian Goetz et al., «Java Concurrency in Practice» — до сих пор лучшая книга о модели памяти.
  • Benjamin Evans, James Gough, Chris Newland, «Optimizing Java» — JIT, GC и измерения.
  • Aleksey Shipilëv — тексты и бенчмарки по JMM, GC и JMH.
  • Java Almanac — что именно изменилось между любыми двумя версиями.

Что дальше

Установка и инструментарий: JDK, Maven, Gradle, структура проекта — выберем дистрибутив JDK, научимся держать несколько версий одновременно, разберёмся, чем Maven отличается от Gradle, и соберём проект, который не стыдно показать команде.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов