Java
Java вже тридцять років є типовою мовою великих бекенд-систем і досі та, яку бачиш, коли банк, авіакомпанія чи телеком відкриває своє дерево коду. Java робить собою не так синтаксис, як машина, на якій вона працює: байт-код замість машинного коду, компілятор, що оптимізує під час роботи програми, і збирач сміття, який налаштовують, а не замінюють. На цій сторінці — JVM, система типів, Spring, паралельність і те, що змінили віртуальні потоки.
- Артефакт
- Байт-код, не машинний
- Пам’ять
- Збирається, не звільняється
- Релізи
- Кожні 6 місяців, LTS раз на 2 роки
Машина під сподом
Java не компілюється в машинний код. Вона компілюється в байт-код — інструкції абстрактної машини, — а JVM під вашу платформу перетворює їх на щось зрозуміле процесору. Ця опосередкованість і породила старе гасло про «написати раз і запускати всюди», і саме через неї той самий артефакт збірки без змін працює і на ноутбуці розробника, і на Linux-сервері.
Усередині JVM спершу інтерпретує цей байт-код — це повільно — і водночас рахує, як часто виконується кожен метод. Коли метод переходить поріг, JIT-компілятор перекладає його в машинний код, і, оскільки він компілює програму, за якою може спостерігати, він оптимізує за фактами, яких статичний компілятор не має ніколи: яку гілку насправді обирають, який тип реально приходить у це місце виклику, який цикл гарячий. Тому Java-сервіс помітно повільніший на перших тисячах запитів, а потім вирівнюється, — і тому бенчмарк на дві секунди не каже про прод нічого.
Друга робота JVM — це пам’ять. Об’єкти виділяються в купі й звільняються збирачем сміття, а не вами, і це прибирає весь клас помилок use-after-free та подвійного звільнення, що панують у C і C++. Збирач спирається на одне сильне спостереження: більшість об’єктів гине майже одразу. Тому купу поділено на покоління: нові об’єкти потрапляють у молоде покоління, яке збирають часто й дуже дешево, а ті нечисленні, що вижили, переходять у старе покоління, яке збирають рідко й дорого. Довгі паузи йдуть саме зі старого покоління, і тому «зменшити сміття» зазвичай означає «перестати підвищувати об’єкти, які мали померти молодими».
Два наслідки визначають, як Java розгортають сьогодні. Старт не миттєвий — JVM має завантажити класи, перевірити їх і прогрітися, — що незручно для функції, яка масштабується до нуля, і байдуже для сервісу, що працює тижнями. А коли старт таки важить, GraalVM native image компілює заздалегідь у справжній виконуваний файл, який стартує за мілісекунди, ціною втрати оптимізацій JIT під час роботи й потреби конфігурувати все, що користується рефлексією.
Мова і скільки її вам треба
Java статично типізована й компільована, тож помилка в назві методу чи не той аргумент — це збій збірки, а не сторінка, що ламається в одного користувача в п’ятницю. Система типів іменна й класова: усе живе в класі, спадкування одиничне, а множинне успадкування поведінки дають інтерфейси, чого класи не дають. Ця багатослівність — угода, на яку мова йшла завжди, і сучасні версії тихо забрали з неї чимало.
Записи
Носій даних в одному рядку: поля, конструктор, аксесори,
equals,hashCodeіtoStringзгенеровано, і він незмінний за побудовою.Запечатані типи
Ієрархія, що називає власні підтипи, тож компілятор знає, що список закритий, і може перевірити switch на повноту.
Зіставлення зі зразком
Перевірити тип і зв’язати його одним кроком, у
instanceofі вswitch, — заміна приведенню після кожної перевірки.Стріми
Декларативна обробка колекцій: filter, map, collect. Читабельно для конвеєра й гірше за цикл для двох операцій.
Optional
Явний тип повернення «може бути відсутнім». Беріть для повернень; не беріть для полів і параметрів — там він лише шум.
var
Локальне виведення типу. Скорочує оголошення, тип якого очевидний із правого боку, і ховає його, коли неочевидний.
Слово про Kotlin, бо це питання виникає завжди. Він працює на тій самій JVM, напряму викликає Java-бібліотеки й виправляє дві головні претензії до Java: можливість null живе в системі типів, а не у вашій голові, і шаблонного коду немає. Для Android це типовий вибір, для нового сервісу — розумний. Це не привід переписувати робочу Java-базу, а змішування обох в одному модулі — витрата на конфігурацію збірки, на яку варто йти свідомо.
Spring і що тут робить фреймворк
Spring на сервері майже універсальний, і по суті він дає впровадження залежностей: ваші класи оголошують у конструкторах, що їм потрібно, а контейнер це постачає. Звучить бюрократично, доки не помічаєш наслідку: класу, який отримує своїх співробітників, у тесті можна дати підроблених, — і бізнес-логіка стає тестованою без бази даних, мережі й запущеного сервера. Spring Boot додає до цього автоконфігурацію з думкою й вбудований сервер, тож застосунок є виконуваним файлом, а не чимось, що розгортають у контейнер сервера застосунків.
| Шар | Що робить | Де кусає |
|---|---|---|
| Контейнер DI | Будує й зв’язує ваші об’єкти, керує їхнім життєвим циклом і областю. | Ін’єкція в поле ховає залежності; просіть їх у конструкторі. |
| Spring MVC / WebFlux | Мапить HTTP на методи. MVC блокує на запит, WebFlux реактивний. | WebFlux окупається, лише якщо неблокуючий увесь ланцюг, разом із драйвером. |
| Spring Data / JPA | Мапить об’єкти на таблиці й виводить запити з імен методів. | Ліниве завантаження перетворює один виклик на N запитів; чесний свідок — лише лог SQL. |
| Транзакції | Анотація відкриває й фіксує транзакцію навколо методу. | Виклик зсередини того самого класу оминає проксі — і анотація не робить нічого. |
Потоки і що змінили віртуальні
Потік у Java завжди був справжнім потоком операційної системи. Це дає справжню паралельність по ядрах — на відміну від PHP, Node чи CPython, Java може завантажити всі ядра в одному процесі — і коштує близько мегабайта стека на потік, тому сервери тримають їх у пулі. Класичний Java-вебсервер дає потік на запит, і межа його паралельності — це розмір пулу: двісті потоків означають двісті запитів у польоті, хоч би більшість із них лише чекала на базу даних.
Віртуальні потоки, стандартні від Java 21, прибирають цю стелю. Ними керує JVM, розкладаючи їх на невелику кількість справжніх потоків; коштують вони кілобайти замість мегабайтів і — ось суть — коли один блокується на вводі-виводі, JVM паркує його й використовує носійний потік для чогось іншого. У підсумку звичайний блокуючий код, написаний прямолінійно, як усі й уміють, масштабується до сотень тисяч одночасних запитів. Тому реактивний стиль, що купував ту саму масштабованість ціною нечитабельних стектрейсів і повністю неблокуючого ланцюга, тепер потрібен значно рідше.
Як це виявляється в реальній поставці
Справжня перевага Java на довгому проєкті — не продуктивність, а те, що інструментарій виходить із припущення, що код переживе тих, хто його написав. Збірка відтворювана, граф залежностей явний, рефакторинг в IDE механічний і безпечний на мільйоні рядків, а профайлер постачається разом із JDK. Flight Recorder і Mission Control покажуть, де на робочому процесі виділення пам’яті й паузи, за ціною, достатньо низькою, щоб лишити їх увімкненими.
Дві операційні нотатки, які варто мати до першого інциденту в проді. Запускайте LTS-реліз — 17 або 21, — бо вони отримують роки безпекових оновлень, тоді як звичайний реліз шість місяців. І задавайте купу явно в контейнері: JVM, яка вважає, що має пам’ять хоста, розрахує купу під хост і буде вбита оркестратором — це виглядає як випадковий перезапуск і аж ніяк не випадкове.
Де це вироджується
- Ліниві зв’язки JPA, завантажені всередині циклу, перетворюють один ендпоінт на сотні запитів.
- Метод із
@Transactional, викликаний із того самого класу: проксі оминають — і ніщо не відкочується. - Винятки, спіймані й залоговані на кожному шарі: один збій дає п’ять стектрейсів і жодного рішення.
- Абстракція, додана під другу реалізацію, яка так і не з’явилася: інтерфейс і фабрика на кожен клас.
- Реактивний стек, узятий заради масштабованості, тоді як один блокуючий драйвер у ланцюгу все серіалізує.
- Розмір купи, лишений типовим усередині контейнера, закінчується вбивством за пам’яттю від оркестратора.
- Бенчмарки, зняті до прогріву JIT, вимірюють інтерпретатор і не доводять нічого.
Коли це застосовувати
Застосовуйте, коли
- Довгоживучі системи зі складними доменами, де статичні типи й безпечний рефакторинг окупаються щороку.
- Сервіси, що мають використати всі ядра в одному процесі: справжні потоки роблять це без додаткових процесів.
- API з високою паралельністю на Java 21: віртуальні потоки дають реактивний масштаб зі звичайним блокуючим кодом.
- Регульовані середовища, де LTS-реліз із роками безпекових патчів — це вимога, а не вподобання.
Уникайте, коли
- Короткоживучі функції, що масштабуються до нуля, — хіба що ви готові будувати й підтримувати native image.
- Маленькі скрипти й разові інструменти, де церемонія коштує більше, ніж уся задача варта.
- Реактивні фреймворки, обрані заради масштабу на Java 21, — віртуальні потоки дають те саме значно простіше.
- Будь-яке середовище, де пауза збирача в мілісекунди справді неприйнятна: це не про це середовище виконання.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.