РозробкаБудь-який рівень

Java

Java — статично типізована мова, що компілюється в байт-код для JVM — середовища, яке компілює гарячий код під час роботи й звільняє пам’ять збирачем сміття. Тридцять років вона є типовим вибором для великих бекенд-систем — банків, авіакомпаній, телекомів, — і найбільше за останні п’ять років змінилося те, як на ній пишуть і як вона чекає: записи, запечатані типи й зіставлення зі зразком у мові, віртуальні потоки в середовищі. Сторінка починається з кожного будівельного блоку на одному екрані — сучасний спосіб поруч із тим, який він замінив; далі — релізи й ліцензії, JVM, пам’ять у контейнері, Spring і його проксі, потоки й віртуальні потоки та п’ять задокументованих кейсів.

Поточний LTS
JDK 25 · вересень 2025
Найновіша
JDK 27 · наступний LTS 29 у 2027
Релізи
Кожні 6 місяців, LTS раз на 2 роки

Java на одному екрані: кожен будівельний блок — тоді й тепер

Java тримає старий код робочим. Більшість класів, написаних під Java 8 у 2014 році, досі компілюються й працюють на JDK 27, тож більшість кодових баз змішує три-чотири епохи, а більшість матеріалів в інтернеті вчить найстарішої. Оскільки новий реліз виходить кожні пів року, замінено чимало. Тут усе це одразу. Таблиця перелічує кожну роботу, те, як її робить сучасна Java, що саме цей спосіб замінив, і реліз JDK, у якому сучасний спосіб став остаточним — не preview, не incubator. Це ще й найшвидший спосіб датувати кодову базу, туторіал чи відповідь кандидата на співбесіді.

РоботаСучасний спосібЩо він замінивJDK
Клас, що лише несе даніrecordПоля, конструктор, геттери, equals і hashCode вручну — або Lombok16
Закритий набір підтипівsealed з permitsВідкрита ієрархія або поле type і коментар17
Розгалуження за типомswitch із зразками типів, перевірений на повнотуЛанцюг if з instanceof, після кожного — приведення21
Розбір запису на частиниЗразки записів: case Point(var x, var y)Виклик аксесорів по одному21
Змінна, яку треба оголосити, але не використати_Ім’я на кшталт ignored22
Багаторядковий текст — SQL, JSONТекстові блоки, """Конкатенація з \n15
Типи локальних зміннихvarТип, написаний з обох боків10
Дати й часjava.time: Instant, LocalDate, ZonedDateTimeDate і Calendar8
Виклик HTTP APIjava.net.http.HttpClient — HTTP/3 з 26HttpURLConnection або сторонній клієнт11
Перший і останній елементgetFirst(), getLast(), reversed()list.get(list.size() - 1)21
Власний крок у стріміStream gatherersЗбір у список і цикл по ньому24
Очікування вводу-виводу в масштабіВіртуальні потокиОбмежений пул потоків або реактивний код21
synchronized навколо блокуючого викликуБезпечний на віртуальних потоках — більше не прив’язує носійПереписування на ReentrantLock24
Контекст на запитScopedValueThreadLocal25
Кілька підзадач як одне цілеСтруктурована паралельність — досі previewExecutorService і список futurespreview
Виклик нативного кодуForeign Function & Memory APIJNI22
Точка входу маленької програмиvoid main() у компактному файлі кодуpublic static void main(String[] args) усередині класу25
Швидший стартAOT-кеш, записаний під час тренувального запускуЗавантаження, лінкування й профілювання з нуля на кожному старті24 · 25
Типовий збирач сміттяG1, у будь-якому середовищіSerial, тихо обраний на одному CPU чи під 1792 МБ27
Заголовок кожного об’єкта8 байтів — компактні заголовки, за замовчуванням12 байтів25 · типово 27

Релізи, підтримка і який JDK у вас насправді

З Java 10, у березні 2018 року, новий JDK виходить щобереня й щовересня. Більшість релізів підтримується лише до наступного, тобто пів року. Раз на два роки один реліз отримує статус — довгострокова підтримка — і роками отримує виправлення безпеки; з 17-ї проміжок між LTS-релізами становить два роки, а не три. Поточний LTS — 25, з вересня 2025 року; найновіший реліз — 27, з вересня 2026 року; наступним LTS буде 29, у вересні 2027 року. Продакшн-системи майже завжди працюють на LTS, і можливості з таблиці доходять до більшості команд стрибками завбільшки з LTS: 11, 17, 21, 25.

JDKВипущеноOracle Premier SupportOracle Extended Support
8 LTSбер. 2014бер. 2022груд. 2030
11 LTSвер. 2018вер. 2023січ. 2032
17 LTSвер. 2021вер. 2026вер. 2029
21 LTSвер. 2023вер. 2028вер. 2031
25 LTSвер. 2025вер. 2030вер. 2033
27вер. 2026бер. 2027—
29 LTSвер. 2027, планвер. 2032вер. 2035

Це дати Oracle для платних клієнтів, і вони не вся історія. JDK — одна кодова база з відкритим кодом, OpenJDK, і кілька постачальників її збирають, тестують і латають: серед них Temurin від Eclipse Adoptium, Amazon Corretto, Azul Zulu, Microsoft і Red Hat. Їхні збірки — той самий код, оновлення для кожного LTS безплатні, і кожен публікує власні дати завершення, зазвичай на кілька років після релізу. Oracle JDK — одна зі збірок, з ліцензією, що раз у раз змінювалася з 2019 року: у квітні 2019 року оновлення Oracle JDK 8 перестали бути безплатними для продакшену; у 2021 році Oracle JDK 17 отримав безплатну ліцензію, що спливає через рік після виходу наступного LTS; у січні 2023 року платна підписка перейшла на ціну за працівника — за кожного працівника й підрядника компанії, а не за сервер чи розробника; а з жовтневого оновлення 2026 року Oracle JDK 21 повертається під ліцензію, що вимагає підписки в продакшені.

Машина під сподом: байт-код, JIT і прогрів

Java не компілюється в машинний код. Компілятор javac перетворює вихідний код на байт-код — інструкції абстрактної машини, — а JVM для кожної платформи перетворює байт-код на те, що виконує процесор. Через цю опосередкованість один JAR-файл без змін працює і на Mac розробника, і на Linux-сервері, і саме звідси береться профіль продуктивності Java.

Java компілюється в байт-код, а не в машинний код, тож артефакт усюди однаковий, а різниться саме JVM під кожну платформу. Усередині інтерпретатор виконує кожен метод, а JIT перекомпільовує в машинний код те, що стало гарячим, — тому сервіс повільний на перших тисячах запитів і швидкий далі. Об’єкти лягають у купу, якою керує середовище виконання: більшість помирає молодими й збирається дешево, ті, що вижили, переходять у старе покоління, і саме його збирання дає паузи.

JVM починає з інтерпретації байт-коду — це повільно — і водночас рахує, як часто виконується кожен метод. Метод, що перейшов поріг, швидко компілює -компілятор першого рівня, лишаючи лічильники, щоб його профілювати. Метод, що лишається гарячим, компілюється знову оптимізувальним компілятором, який спирається на цей профіль: яку гілку насправді обирають, який клас реально приходить у цей виклик, який цикл виконується мільйон разів. Якщо припущення згодом виявляється хибним — з’явився новий підклас, — JVM викидає оптимізований код і повертається на рівень нижче. Тому Java-сервіс помітно повільніший на перших кількох тисячах запитів, а потім вирівнюється, і тому його пікова швидкість може перевершити код, скомпільований заздалегідь.

Старт — друга ціна: JVM має завантажити, перевірити й злінкувати тисячі класів, перш ніж щось обслужити. Для сервісу, що працює тижнями, це майже не важить, а для функції, що масштабується до нуля, важить дуже. Тепер є чотири відповіді, кожна зі своєю ціною:

ВаріантЩо робитьЧого коштує
Звичайна JVMЗавантажує й лінкує на кожному старті, потім прогріваєтьсяСекунди на старт; нічого налаштовувати
AOT-кеш, JDK 24+Тренувальний запуск записує завантажені класи, а з 25 — профілі методів; наступні старти їх перевикористовуютьТренувальний крок у збірці; кеш має відповідати JDK і classpath
CRaCЗберігає прогрітий процес на диск і відновлює йогоЛише Linux; відкриті файли й з’єднання треба закривати й відкривати навколо збереження
GraalVM Native ImageКомпілює весь застосунок у виконуваний файл заздалегідьСтартує за мілісекунди, але без JIT під час роботи, з повільною збіркою й конфігурацією для рефлексії

Пам’ять: купа, збирач і контейнер навколо них

Об’єкти живуть у купі й звільняються збирачем сміття, а не вами, і це прибирає помилки use-after-free та подвійного звільнення, що панують у C і C++. Збирачі спираються на одне сильне спостереження: більшість об’єктів гине майже одразу. Тому купу поділено на покоління. Нові об’єкти йдуть у молоде покоління, яке збирають часто й дешево; ті нечисленні, що вижили, переходять у старе покоління, яке збирають рідко й дорого. Довгі паузи йдуть зі старого покоління, тож «зменшити сміття» зазвичай означає «перестати підвищувати об’єкти, які мали померти молодими».

ЗбирачЯк він зупиняє застосунокПідходить для
SerialЗупиняє все й збирає в одному потоціКрихітні купи й утиліти командного рядка
ParallelЗупиняє все й збирає на всіх ядрахПакетні задачі, де важить лише загальна пропускна здатність
G1 — типовийКороткі паузи з ціллю, типово 200 мсМайже кожен сервіс
ZGCПаузи менше мілісекунди за будь-якого розміру купиСервіси, чутливі до затримок, і дуже великі купи; коштує додаткового CPU й пам’яті
ShenandoahПаралельний, з малими паузами; поколіннєвий з 25Та сама ніша, що й ZGC; у більшості збірок OpenJDK, не в Oracle JDK

На практиці збирач важить менше, ніж контейнер навколо JVM. З JDK 10 JVM читає ліміт пам’яті контейнера, а не хоста, і типово бере на купу чверть від нього: контейнер на 4 ГБ отримує купу на 1 ГБ, і більша частина пам’яті, за яку ви платите, не використовується ніколи. Поширене виправлення робить протилежну помилку. Купа, рівна ліміту контейнера, — -Xmx4g у контейнері на 4 ГБ, — не лишає місця всьому, що JVM тримає поза купою: метаданим класів, скомпільованому коду, стеку для кожного платформного потоку, мережевим буферам, власному обліку збирача. Процес переростає контейнер, і ядро його вбиває.

JVM використовує пам’ять і поза купою: метадані класів, скомпільований код, стек для кожного платформного потоку, мережеві буфери й облік збирача сміття. У контейнері на 4 ГБ купа, рівна ліміту, не лишає місця нічому з цього, тож увесь процес переростає контейнер, і ядро його вбиває — код виходу 137, жодного стектрейсу Java. Із типовим налаштуванням JVM бере на купу чверть ліміту — це безпечно, але більша частина пам’яті лишається невикористаною. Відсоток близько 75 вміщує і те, і те. 0,6 ГБ поза купою — ілюстративна цифра; справжню вимірює Native Memory Tracking.

Spring: впровадження, автоконфігурація і проксі, якого не видно

Spring майже універсальний на Java-сервері, і по суті він дає впровадження залежностей: клас оголошує в конструкторі, що йому потрібно, а контейнер це постачає. Звучить як бюрократія, доки не побачиш наслідку: класу, що отримує своїх співробітників, у тесті можна дати підробки, тож бізнес-логіка тестується без бази даних, мережі й запущеного сервера. Spring Boot додає автоконфігурацію, яка дивиться на бібліотеки в classpath і зв’язує розумні типові налаштування, та вбудований вебсервер, тож застосунок — це один виконуваний JAR. Boot 4.0, випущений у листопаді 2025 року на Spring Framework 7, вимагає Java 17 чи новішої.

ШарЩо робитьДе кусає
КонтейнерБудує й зв’язує ваші об’єкти й керує тим, скільки живе коженВпровадження в поля ховає, що потрібно класу; просіть у конструкторі
АвтоконфігураціяЗв’язує типові налаштування з того, що є в classpathДодавання однієї залежності може ввімкнути можливість; --debug друкує звіт умов
Spring MVC / WebFluxМапить HTTP на методи: MVC блокує на запит, WebFlux реактивнийWebFlux окупається, лише якщо неблокуючий увесь ланцюг, разом із драйвером
Spring Data JPAМапить об’єкти на таблиці й виводить запити з імен методівЛіниві зв’язки перетворюють один виклик на N запитів; чесний свідок — лише лог SQL
Транзакції, кешування, async@Transactional, @Cacheable, @Async навколо методуВони працюють через проксі, і виклик повз проксі оминає й їх
Spring реалізує @Transactional, @Cacheable і @Async через проксі — згенерований об’єкт, який інші біни тримають замість вашого; він робить свою роботу, а тоді викликає ваш метод. Виклик з іншого біна йде через нього, тож placeOrder виконується в транзакції. Виклик із placeOrder у writeAudit того самого класу йде на this, справжній об’єкт, і проксі не торкається — тож REQUIRES_NEW не відкриває нової транзакції, а запис аудиту відкочується разом із замовленням. Жодного попередження в лозі. Винесення writeAudit в окремий бін повертає проксі на шлях виклику.

Власне вікно підтримки Spring Boot коротше, ніж у Java. Кожна мінорна лінія отримує приблизно рік безплатних виправлень: 3.5 втратила безплатну підтримку 30 червня 2026 року, 4.0 має її до 31 грудня 2026 року, а 4.1, випущена в червні 2026 року, — до 31 липня 2027 року. Після цього виправлення — комерційні. Команда, що оновлює Java раз на два роки і Spring Boot раз на два роки, половину цього часу перебуває поза безплатним вікном.

Потоки і що змінили віртуальні

Потік у Java завжди був потоком операційної системи. Це дає справжню паралельність по ядрах — на відміну від PHP, Node.js чи CPython, один процес Java може завантажити всі ядра, — і коштує стеку близько мегабайта на потік, тому сервери тримають потоки в пулі. Класичний вебсервер на Java дає один потік на запит, і межа його паралельності — пул: Tomcat, вбудований у Spring Boot, типово має 200 потоків. Двісті запитів у польоті, хоч би скільки з них лише чекало на базу даних.

Віртуальні потоки, остаточні в JDK 21, прибирають цю стелю. JVM розкладає їх на невелику кількість потоків операційної системи, що звуться носіями (carriers), — типово по одному на ядро, — а коштує віртуальний потік значно менше за платформний: його стек живе в купі й росте лише на ту глибину, на яку його використали. Коли віртуальний потік блокується на вводі-виводі, JVM його знімає: стек відкладається в купу, а носій бере інший віртуальний потік. Звичайний блокуючий код, написаний у простому стилі, який знають усі, масштабується до сотень тисяч одночасних запитів. У Spring Boot це одна властивість, spring.threads.virtual.enabled=true. Тому реактивний стиль, що купував той самий масштаб ціною повністю неблокуючого ланцюга й нечитабельних стектрейсів, потрібен значно рідше, ніж раніше.

Віртуальні потоки, остаточні в JDK 21, виконуються на кількох потоках операційної системи — носіях, типово по одному на ядро. Коли віртуальний потік блокується на вводі-виводі, JVM знімає його, тримає його стек у купі й віддає носій іншому віртуальному потоку; коли приходить відповідь, потік знову монтується, можливо, на інший носій. Так звичайний блокуючий код масштабується до сотень тисяч одночасних запитів. Прикутий віртуальний потік зняти не можна, і він тримає свій носій, поки чекає; коли носіїв стільки, скільки ядер, кілька прикутих потоків можуть зупинити решту. На JDK 21–23 це траплялося всередині synchronized; JDK 24 це виправив, а нативний код та ініціалізатори класів досі прикувають.

Віртуальний потік, якого не можна зняти, називають прикутим (pinned), і він тримає свій носій, поки чекає. На JDK 21–23 це траплялося щоразу, коли потік блокувався всередині блоку synchronized — звичного в старіших бібліотеках, — а з носіями по числу ядер кілька прикутих потоків могли зупинити застосунок; це п’ятий кейс нижче. JDK 24 виправив synchronized; потік досі прикутий, поки він усередині нативного коду чи ініціалізатора класу. Увімкнення віртуальних потоків змінює більше, ніж кількість потоків, і більшість змін переносить вузьке місце, а не прибирає його:

Що булоЩо змінюється з віртуальними потокамиЩо робити
Пул на 200 потоків як межаМежі немає; 10 000 запитів тепер одночасно доходять до базиОбмежувати дефіцитне через Semaphore, а не пулом потоків
Пул з’єднань на 10 (типово в HikariCP)Стає вузьким місцем, і потоки стають до нього в чергуРозраховувати під базу й задати короткий тайм-аут очікування
Кеш у ThreadLocal на потікМільйон потоків тримає мільйон копійScopedValue для контексту; спільний кеш для об’єктів
Пулінг потоків, щоб зекономити на нихБезглуздо — віртуальний потік дешевший за місце в пуліОдин новий віртуальний потік на задачу
synchronized навколо блокуючого викликуПрикуває носій на 21–23; нормально з 24Працювати на 24 чи новішій, або на 25 як LTS
Робота, обмежена процесоромНічого — межа досі в кількості ядерЛишити на обмеженому пулі платформних потоків

П’ять кейсів і те, чого в кожному бракувало

Кожен кейс нижче задокументовано в бюлетені чи дорожній карті самого постачальника, державному звіті або інженерному розборі команди, з якою це сталося. Три — провали безпеки, один — витрата, один — аварія, і в чотирьох із п’яти бракувало знання про те, що насправді працює.

КейсЩо сталосяЧого бракувало
Ґаджети десеріалізації, 2015Звичайні бібліотечні класи, десеріалізовані з мережі, виконували команди зловмисниківПравило ніколи не десеріалізувати недовірені байти
Equifax, 2017Ваду Struts, виправлену в березні, експлуатували з травня; дані 147 мільйонів людей викраденоРеєстр того, де працює кожна бібліотека
Log4Shell, груд. 2021Бібліотека логування виконувала підстановки всередині тексту, який логувалаSBOM і жодних вихідних з’єднань із серверів
Ліцензії Oracle, 2019–2026Звичайні оновлення переходили на платні умови, з 2023 року — з ціною за працівникаЗнання, яка збірка JDK працює в продакшені
Netflix, лип. 2024Віртуальні потоки прикули всі носії, і екземпляри перестали обслуговувати трафікСтеження за прикуванням до розгортання — і JDK 24

Другий і третій кейси — той самий провал із різницею в чотири роки. Apache Struts, вебфреймворк на Java, оприлюднив і виправив -2017-5638 7 березня 2017 року. Команда безпеки Equifax розіслала попередження, але систему на Struts — онлайн-портал, де споживачі оскаржували свої кредитні записи, — не оновили, а сканування, що мало це впіймати, її не знайшло. Зловмисники зайшли 13 травня й пробули 76 днів; дані близько 147 мільйонів людей викрадено. Державні звіти, що вийшли після, читаються як перелік відсутнього обліку: компанія не могла надійно сказати, які з її систем використовують бібліотеку. У грудні 2021 року Log4Shell поставив те саме питання всім організаціям одночасно. Log4j 2, найпоширеніша бібліотека логування в Java, обчислювала підстановки ${jndi:ldap://…}, записані всередині повідомлень, які вона логувала, тож будь-який рядок, який зловмисник міг змусити залогувати, — ім’я користувача, заголовок User-Agent, — змушував сервер з’єднатися з машиною зловмисника і, у багатьох конфігураціях, завантажити й виконати з неї код. Оцінка — 10 із 10. Латання зайняло чотири релізи за три тижні, і повільною частиною всюди було не латання, а пошук: Log4j зазвичай приходила як залежність залежності.

Четвертий кейс — не вразливість, і деяким організаціям він коштував більше за неї. Кожна зміна ліцензування Oracle, перелічена в розділі про релізи, приходила як звичайне оновлення того самого продукту. Організації, що стандартизувалися на Oracle JDK, бо це було типове завантаження, могли на аудиті з’ясувати, що оновлення, які вони ставили, ліцензуються за кожного працівника всієї компанії. Команди, що стандартизувалися на збірці OpenJDK — тому самому коді, — не помітили нічого. Захід був той самий, якого бракувало Equifax і за Log4Shell, лише застосований до середовища виконання, а не до бібліотеки: знання, що насправді працює.

Де вона вироджується

Справжня перевага Java на довгому проєкті — не швидкість. Вона в тому, що інструменти виходять із припущення, що код переживе тих, хто його написав: збірка відтворювана, граф залежностей явний, рефакторинг на мільйоні рядків механічний і безпечний, а продакшн-профайлер, Flight Recorder, постачається з JDK за ціною, достатньо низькою, щоб лишати його ввімкненим. Коли Java-система вироджується, рідко винна мова. Винні середовище на налаштуваннях, яких ніхто не обирав, анотація, якій довірили те, чого вона не бачить, або залежність, про яку ніхто не знав:

  • Купа, рівна ліміту контейнера, що закінчується перезапуском з OOMKilled без стектрейсу.
  • Купа, лишена на типовій чверті, — оплата пам’яті, якою JVM ніколи не користується.
  • Ліниві зв’язки JPA, завантажені в циклі чи у view через open-in-view, перетворюють один ендпоінт на сотні запитів.
  • Метод із @Transactional, викликаний із власного класу: проксі оминають, і нічого не відкочується так, як задумано.
  • Віртуальні потоки, увімкнені з пулом на десять з’єднань, — черга переїжджає, а не зникає.
  • Реактивний стек, узятий заради масштабу на Java 21 чи новішій, де звичайний блокуючий код тепер доходить туди ж.
  • Винятки, спіймані й залоговані на кожному шарі: один збій дає п’ять стектрейсів і жодного рішення.
  • Інтерфейс і фабрика на кожен клас — під другу реалізацію, яка так і не з’являється.
  • Вбудована серіалізація, що приймає байти ззовні процесу.
  • JDK чи лінія Spring Boot поза вікном підтримки — або збірка Oracle, яку ніхто не обирав.

Чесна межа — у самій формі середовища. JVM збудовано, щоб стартувати раз і працювати довго, і кожна відповідь на швидкий старт із розділу про JVM чимось за нього розплачується. Збирач сміття, навіть з паузами менше мілісекунди, однаково означає накладні витрати пам’яті й процес, таймінг якого не повністю під вашим контролем. А церемонія, що окупається за десятиліття, коштує реального часу на двотижневому прототипі чи скрипті на п’ятдесят рядків, де легший інструмент просто кращий вибір.

Практична перевірка — три команди проти запущеного продакшн-екземпляра. java -version дає реліз і постачальника — таблицю підтримки й ліцензію. jcmd <pid> VM.flags показує збирач і купу, які JVM насправді обрала. А дерево залежностей із mvn dependency:tree чи gradle dependencies, збережене як , — та відповідь, що знадобиться вам зранку наступного Log4Shell.

Коли це застосовувати

Застосовуйте, коли

  • Довгоживучі системи зі складними доменами, де статичні типи й безпечний рефакторинг окупаються щороку.
  • Сервіси, що мають використати всі ядра в одному процесі: справжні потоки роблять це без додаткових процесів.
  • з високою паралельністю на JDK 25: віртуальні потоки дають реактивний масштаб зі звичайним блокуючим кодом.
  • Регульовані середовища, де LTS-реліз із роками виправлень безпеки — вимога, а не вподобання.

Уникайте, коли

  • Короткоживучі функції, що масштабуються до нуля, — хіба що ви готові будувати й підтримувати native image чи AOT-кеш.
  • Маленькі скрипти й разові інструменти, де церемонія коштує більше, ніж уся задача.
  • Реактивні фреймворки, обрані заради масштабу на JDK 21 чи новішій, — віртуальні потоки дають те саме простіше.
  • Будь-яке середовище, де непередбачувана пауза навіть у мілісекунду неприйнятна: це не про це середовище.

Джерела й метод

На чому стоїть ця стаття

  • 16 джерел
  • 4 задокументовані збої
  • Перевірено 13 жовтня 2026 р.

Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.