- PVSM.RU - https://www.pvsm.ru -
Java 21 является LTS-релизом, а значит у него будут выходить обновления как минимум 5 лет [5] с момента выхода.
Скачать JDK 21 можно по этим ссылкам:
Вот список JEP'ов, которые попали в Java 21.
Pattern Matching for switch
(JEP 441) [10]Паттерн-матчинг для switch
наконец-то был финализирован и стал стабильной конструкцией языка. Напомним, что он появился в Java 17 [11] и был в состоянии preview [12] четыре релиза: 17 [13], 18 [14], 19 [15] и 20 [16].
Новый паттерн-матчинг существенно расширяет возможности оператора switch
. Начиная с Java 1.0, switch
поддерживал только сравнение с примитивными константами. Позже список типов был расширен (Java 5 – перечисления, Java 7 – строки), но в ветках case
всё ещё могли быть только константы.
Теперь же switch
поддерживает в ветках case
так называемые паттерны:
Object obj = …
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
Паттерны могут снабжаться условиями с использованием нового ключевого слова when
:
Object obj = …
return switch (obj) {
case Integer i when i > 0 -> String.format("positive int %d", i);
case Integer i -> String.format("int %d", i);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
Также добавлена поддержка матчинга null
. Сделать это можно с помощью явной отдельной ветки case null
:
Object obj = …
switch (obj) {
case null -> System.out.println("Null");
case String s -> System.out.println("String: " + s);
default -> System.out.println("Other");
}
Если ветка case null
отсутствует, то switch
с переданным в него null
всегда будет выбрасывать NullPointerException
(даже если есть ветка default
):
Object obj = null;
switch (obj) { // NullPointerException
case String s -> System.out.println("String: " + s);
default -> System.out.println("Other");
}
Ветки null
и default
можно объединять друг с другом:
String str = …
switch (str) {
case "Foo", "Bar" -> System.out.println("Foo or Bar");
case null, default -> System.out.println("Null or other");
}
Новый паттерн-матчинг обладает рядом ограничений.
Во-первых, все switch
(кроме тех, что были корректными до Java 21) должны быть исчерпывающими. Т.е. в ветках должны покрываться все возможные случаи:
Object obj = …
switch (obj) { // error: the switch statement does not cover all possible input values
case String s -> System.out.println(s.length());
case Integer i -> System.out.println(i);
};
Пример выше можно исправить, добавив ветку Object o
или default
.
Во-вторых, все ветки case
должны располагаться в таком порядке, что ни перед одной веткой нет доминирующей ветки:
return switch (obj) {
case CharSequence cs ->
"sequence of length " + cs.length();
case String s -> // error: this case label is dominated by a preceding case label
"string of length " + s.length();
default -> "other";
};
Так как CharSequence
это более широкий тип, чем String
, то его ветка должна быть расположена ниже.
В-третьих, несколько паттернов в одной ветке работать не будут:
return switch (obj) {
case String s, Integer i -> "string or integer"; // error: illegal fall-through from a pattern
default -> "other";
};
Т.е. сделать тест по нескольким типам в одной ветке пока что нельзя (хотя грамматика языка это позволяет). Это можно обойти, только включив режим preview и заменив s
и i
на символы подчёркивания (см. JEP про безымянные переменные ниже).
В целом новый паттерн-матчинг значительно увеличивает выразительность языка. Особенно хорошо он сочетается с записями. Паттерны записей мы рассмотрим отдельно, поскольку про них есть свой собственный JEP (см. следующий раздел).
Отдельным видом паттернов являются паттерны записей. Они появились в Java 19 [18] в режиме preview и стали стабильными в Java 21.
Паттерны записей позволяют осуществлять деконструкцию значений записей чрезвычайно компактно:
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x + y);
}
}
Или через оператор switch
:
static void printSum(Object obj) {
switch (obj) {
case Point(int x, int y) -> System.out.println(x + y);
default -> System.out.println("Not a point");
}
}
Особая мощь паттернов записей состоит в том, что они могут быть вложенными:
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
System.out.println(c);
}
}
Используя var
, можно сократить код ещё сильнее:
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(var p, var c), var lr)) {
System.out.println(c);
}
}
Паттерны записей отлично сочетаются с паттернами по типу:
record Box(Object obj) {}
static void test(Box box) {
switch (box) {
case Box(String s) -> System.out.println("string: " + s);
case Box(Object o) -> System.out.println("other: " + o);
}
}
Поддерживается вывод типов записей-дженериков:
record Box<T>(T t) {}
static void test(Box<Box<String>> box) {
if (box instanceof Box(Box(var s))) { // Infers Box<Box<String>>(Box<String>(String s))
System.out.println("String " + s);
}
}
К сожалению, паттерны записей могут использоваться только в instanceof
и switch
, но не могут использоваться сами по себе:
static void usePoint(Point p) {
Point(var x, var y) = p; // Не сработает
// Use x and y
}
Будем надеяться, что когда-нибудь добавят и такую возможность.
Строковые шаблоны – новая синтаксическая возможность, позволяющая встраивать в строки выражения:
int x = 10;
int y = 20;
String str = STR."{x} plus {y} equals {x + y}";
// В str будет лежать "10 + 20 equals 30"
Таким образом, в Java появилась строковая интерполяция, которая уже давно есть во многих других известных языках программирования. Однако в Java она работает только в режиме preview [12], т.е. использовать в Java 21 её можно только с включенным флагом --enable-preview
.
Реализация строковых шаблонов в Java отличается от большинства реализаций в других языках: в Java строковый шаблон на самом деле сначала превращается в объект java.lang.StringTemplate
[20], а затем процессор, реализующий java.lang.StringTemplate.Processor
[21], конвертирует этот объект в строку (или объект другого класса). В примере выше STR."…"
есть ничто иное, как сокращённый вариант следующего кода:
StringTemplate template = RAW."{x} plus {y} equals {x + y}";
String str = STR.process(template);
STR
[22] – это стандартный и наиболее часто используемый процессор, который выполняет простую подстановку значений в шаблон и возвращает сконкатенированную строку. STR
неявно импортируется в любой исходный файл, поэтому его можно использовать без import
.
RAW
[23] – это процессор, который ничего не делает со StringTemplate
и просто возвращает его. Обычно он не используется, т.к. на практике мало кому нужны сырые представления шаблонов, а нужны результаты интерполяции в виде готовых объектов.
Процессоры были введены для того, чтобы была возможность кастомизировать процесс интерполяции. Например, ещё один стандартный процессор FMT
[24] поддерживает форматирование с использованием спецификаторов, определённых в java.util.Formatter
[25]:
double length = 46;
System.out.println(FMT."The length is %.2f{length} cm");
// The length is 46.00 cm
Процессоры необязательно должны возвращать String
. Вот общая сигнатура метода process()
[26] интерфейса Processor
:
public interface Processor<R, E extends Throwable> {
R process(StringTemplate stringTemplate) throws E;
}
Это значит, что можно реализовать процессор, который будет делать практически всё что угодно и возвращать что угодно. Например, гипотетический процессор JSON
будет создавать напрямую объекты JSON (без промежуточного объекта String
) и при этом поддерживать экранирование кавычек:
JSONObject doc = JSON."""
{
"name": "{name}",
"phone": "{phone}",
"address": "{address}"
};
""";
Если в name
, phone
или address
будут содержаться кавычки, то они не испортят объект, т.к. процессор заменит "
на "
.
Или, например, процессор SQL
будет создавать PreparedStatement'ы, защищая от атак SQL Injection:
PreparedStatement ps = SQL."SELECT * FROM Person p WHERE p.name = {name}";
Таким образом, строковые шаблоны гораздо более мощный инструмент, нежели простая конкатенирующая строковая интерполяция. Они решают не только проблему простого внедрения выражений в строки и увеличивают читабельность, но и улучшают безопасность и гибкость программ.
Ещё одно новшество в режиме preview: теперь можно объявлять так называемые безымянные переменные и паттерны. Делается это с помощью символа подчеркивания (_
). Это часто необходимо, когда переменная или паттерн не используются:
int acc = 0;
for (Order _ : orders) {
if (acc < LIMIT) {
… acc++ …
}
}
В примере выше важен факт наличия элемента, но сама переменная не нужна. Поэтому, чтобы не придумывать этой переменной название, было использовано подчеркивание вместо имени.
Довольно частый пример нужности безымянных переменных – блок catch
с неиспользуемым исключением:
String s = …
try {
int i = Integer.parseInt(s);
…
} catch (NumberFormatException _) {
System.out.println("Bad number: " + s);
}
Полный список случаев, в которых можно использовать безымянные переменные:
try-with-resources
,for
statement,for
,catch
,Внимательный читатель заметит, что в списке выше отсутствуют параметры методов. Действительно, они не могут быть безымянными, и для любых методов (как интерфейсов, так и классов) по-прежнему всегда нужно указывать имена параметров.
Символы подчёркивания также можно использовать для указания безымянных паттернов:
if (r instanceof ColoredPoint(Point(int x, int y), _)) {
// Используются только x и y
}
Здесь разработчику понадобились только координаты точки, но не её цвет. Без безымянного паттерна ему пришлось бы объявлять неиспользуемую переменную типа Color
и придумывать ей имя:
if (r instanceof ColoredPoint(Point(int x, int y), Color c)) { // Warning: unused c
// Используются только x и y
}
Такой код менее читабелен и хуже позволяет сфокусироваться на главном (координатах). Кроме того, некоторые IDE подсветили бы неиспользуемую переменную c
, что ещё одно дополнительное неудобство.
Есть также возможность объявлять безымянные переменные паттернов:
if (r instanceof ColoredPoint(Point(int x, int y), Color _)) {
…
}
Безымянные паттерны и переменные паттернов прекрасно сочетаются и со switch
:
switch (box) {
case Box(RedBall _), Box(BlueBall _) -> processBox(box);
case Box(GreenBall _) -> stopProcessing();
case Box(_) -> pickAnotherBox();
}
В целом, паттерн-матчинг и безымянные паттерны вместе обладают большой синергией и позволяют писать действительно мощные, компактные и выразительные конструкции.
Теперь в режиме preview можно запускать программы с методами main()
, которые не являются public static
и у которых нет параметра String[] args
:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
В таком случае JVM сама создаст экземпляр класса (у него должен быть не-private
конструктор без параметров) и вызовет у него метод main()
.
Протокол запуска будет выбирать метод main()
согласно следующему приоритету:
static void main(String[] args)
static void main()
void main(String[] args)
void main()
Кроме того, можно писать программы и без объявления класса вовсе:
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
В таком случае будет создан неявный безымянный класс (не путать с анонимным классом), которому будут принадлежать метод main()
и другие верхнеуровневые объявления в файле:
// class <some name> { ← неявно
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
// }
Безымянный класс является синтетическим [29] и final
. Его simple name [30] является пустой строкой:
void main() {
System.out.println(getClass().isUnnamed()); // true
System.out.println(getClass().isSynthetic()); // true
System.out.println(getClass().getSimpleName()); // ""
System.out.println(getClass().getCanonicalName()); // null
}
При этом имя [31] класса совпадает с именем файла, но такое поведение не гарантируется.
Такое упрощение запуска Java-программ было сделано с двумя целями:
Виртуальные потоки, которые много лет разрабатывались в рамках проекта Loom [33] и появились в Java 19 [34] в режиме preview, теперь наконец-то стали стабильными.
Виртуальные потоки, в отличие от потоков операционной системы, являются легковесными и могут создаваться в огромном количестве (миллионы экземпляров). Это свойство должно значительно облегчить написание конкурентных программ, поскольку позволит применять простой подход «один запрос – один поток» (или «одна задача – один поток») и не прибегать к более сложным асинхронному или реактивному программированию. При этом миграция на виртуальные потоки уже существующего кода должна быть максимально простой, потому что виртуальные потоки являются экземплярами существующего класса java.lang.Thread
[35] и практически полностью совместимы с классическими потоками: поддерживают стек-трейсы, interrupt()
[36], ThreadLocal
[37] и т.д.
Виртуальные потоки реализованы поверх обычных потоков и существуют только для JVM, но не для операционной системы (отсюда и название «виртуальные»). Поток, на котором в данный момент выполняется виртуальный поток, называется потоком-носителем. Если потоки платформы полагаются на планировщик операционной системы, то планировщиком для виртуальных потоков является ForkJoinPool
[38]. Когда виртуальный поток блокируется на некоторой блокирующей операции, то он размонтируется от своего потока-носителя, что позволяет потоку-носителю примонтировать другой виртуальный поток и продолжить работу. Такой режим работы и дешевизна виртуальных потоков позволяет им очень хорошо масштабироваться. Однако на данный момент есть два исключения: synchronized
блоки и JNI. При их выполнении виртуальный поток не может быть размонтирован, поскольку он привязан к своему потоку-носителю. Такое ограничение может препятствовать масштабированию. Поэтому при желании максимально использовать потенциал виртуальных потоков рекомендуется избегать synchronized
блоков и операции JNI, которые выполняются часто или занимают длительное время.
Несмотря на привлекательность виртуальных потоков, вовсе необязательно предпочитать только их и всегда избегать классических потоков. Например, для задач, интенсивно и долго использующих CPU, лучше подойдут обычные потоки. Или если нужен поток, не являющийся демоном [39], то также придётся использовать обычный поток, потому что виртуальный поток всегда является демоном.
Для создания виртуальных потоков и работы с ними появилось следующее API:
Thread.Builder
[40] – билдер потоков. Например, виртуальный поток можно создать путём вызова Thread.ofVirtual().name("name").unstarted(runnable)
.Thread.startVirtualThread(Runnable)
[41] – создаёт и сразу же запускает виртуальный поток.Thread.isVirtual()
[42] – проверяет, является ли поток виртуальным.Executors.newVirtualThreadPerTaskExecutor()
[43] – возвращает исполнитель, который создаёт новый виртуальный поток на каждую задачу.Для виртуальных потоков также добавилась поддержка в инструментарии JDK (дебаггер, JVM TI, Java Flight Recorder).
Появились три новых интерфейса SequencedCollection
[45], SequencedSet
[46] и SequencedMap
[47].
SequencedCollection
является наследником Collection
[48] и представляет собой коллекцию с установленным порядком элементов. Такими коллекциями являются LinkedHashSet
[49] и все реализации List
[50], SortedSet
[51] и Deque
[52]. У этих коллекций есть общее свойство последовательности элементов, но до Java 21 их общим родителем был Collection
, который является слишком общим интерфейсом и не содержит многих методов, характерных для последовательностей (getFirst()
, getLast()
, addFirst()
, addLast()
, reversed()
и т.д). При этом у самих вышеописанных коллекций такие методы были несогласованны друг с другом (например, list.get(0)
против sortedSet.first()
против deque.getFirst()
), либо вовсе отсутствовали (например, linkedHashSet.getLast()
).
SequencedCollection
закрыла эту дыру в иерархии и привела API к общему знаменателю:
interface SequencedCollection<E> extends Collection<E> {
E getFirst();
E getLast();
void addFirst(E);
void addLast(E);
E removeFirst();
E removeLast();
SequencedCollection<E> reversed();
}
Теперь больше не надо думать, как для конкретной коллекции получить последний элемент, потому что есть универсальный метод getLast()
[53], который есть и у ArrayList
, и у TreeSet
, и у ArrayDeque
.
Особый интерес представляет метод reversed()
[54], который возвращает view коллекции с обратным порядком. Это делает обратный обход коллекции гораздо более лаконичным:
var linkedList = new LinkedList<>(…);
// До Java 21
for (var it = linkedList.descendingIterator(); it.hasNext();) {
var e = it.next();
…
}
// С Java 21
for (var element : linkedList.reversed()) {
…
}
Для LinkedHashSet
эффективного способа обратного обхода и вовсе не было.
Для последовательных множеств ввели интерфейс SequencedSet
:
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed();
}
Его реализациями являются LinkedHashSet
и наследники SortedSet
.
Также ввели интерфейс SequencedMap
:
interface SequencedMap<K,V> extends Map<K,V> {
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
V putFirst(K, V);
V putLast(K, V);
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
SequencedMap<K,V> reversed();
}
Его реализациями являются LinkedHashMap
[55] и наследники SortedMap
[56].
Scoped Values, которые появились в Java 20 [58] в инкубационном статусе [59], теперь стали Preview API.
Новый класс ScopedValue
[60] позволяет обмениваться иммутабельными данными без их передачи через аргументы методов. Он является альтернативой существующему классу ThreadLocal
[37].
Классы ThreadLocal
и ScopedValue
похожи тем, что решают одну и ту же задачу: передать значение переменной в рамках одного потока (или дерева потоков) из одного места в другое без использования явного параметра. В случае ThreadLocal
для этого вызывается метод set()
[61], который кладёт значение переменной для данного потока, а потом метод get()
[62] вызывается из другого места для получения значения переменной. У данного подхода есть ряд недостатков:
set()
можно вызвать когда угодно и откуда угодно).ThreadLocal.remove()
, но про него часто забывают).Эти проблемы усугубляются с появлением виртуальных потоков, которые могут создаваться в гораздо большем количестве, чем обычные.
ScopedValue
лишён вышеперечисленных недостатков. В отличие от ThreadLocal
, ScopedValue
не имеет метода set()
. Значение ассоциируется с объектом ScopedValue
путём вызова другого метода where()
[63]. Далее вызывается метод run()
[64], на протяжении которого это значение можно получить (через метод get()
[65]), но нельзя изменить. Как только исполнение метода run()
заканчивается, значение отвязывается от объекта ScopedValue
. Поскольку значение не меняется, решается и проблема дорогого наследования: дочерним потокам не надо копировать значение, которое остаётся постоянным в течение периода жизни.
Пример использования ScopedValue
:
private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.where(CONTEXT, context)
.run(() -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
В целом ScopedValue
является предпочтительной заменой ThreadLocal
, т.к. навязывает разработчику безопасную однонаправленную модель работы с неизменяемыми данными. Однако такой подход не всегда неприменим для некоторых задач, и для них ThreadLocal
может быть единственно возможным решением.
Ещё одно API, которое ранее было в инкубационном статусе (Java 19 [67] и 20 [68]), а теперь стало Preview API – это Structured Concurrency.
Structured Concurrency – это подход многопоточного программирования, который заимствует принципы из однопоточного структурного программирования. Главная идея такого подхода заключается в следующем: если задача расщепляется на несколько конкурентных подзадач, то эти подзадачи воссоединяются в блоке кода главной задачи. Все подзадачи логически сгруппированы и организованы в иерархию. Каждая подзадача ограничена по времени жизни областью видимости блока кода главной задачи.
В центре нового API класс StructuredTaskScope
[69], у которого есть два главных метода:
fork()
[70] – создаёт подзадачу и запускает её в новом виртуальном потоке,join()
[71] – ждёт, пока не завершатся все подзадачи или пока scope не будет остановлен [72].Пример использования StructuredTaskScope
, где показана задача, которая параллельно запускает две подзадачи и дожидается результата их выполнения:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both forks
.throwIfFailed(); // ... and propagate errors
return new Response(user.get(), order.get());
}
Может показаться, что в точности аналогичный код можно было бы написать с использованием классического ExecutorService
[73] и submit()
[74], но у StructuredTaskScope
есть несколько принципиальных отличий, которые делают код безопаснее:
try-with-resources
. Метод close()
[75] гарантированно не завершится, пока не завершатся все подзадачи.findUser()
и fetchOrder()
завершается ошибкой, то другая операция отменяется автоматически, если ещё не завершена (в случае политики ShutdownOnFailure
, возможны другие).join()
, то обе операции findUser()
и fetchOrder()
отменяются при выходе из блока.findUser()
и fetchOrder()
, будут отображаться как дочерние для главного потока.Structured Concurrency должно облегчить написание безопасных многопоточных программ благодаря знакомому структурному подходу.
Foreign Function & Memory API, ставшее preview в Java 19 [77], продолжает находиться в этом статусе. API находится в пакете java.lang.foreign
[78].
Напомним, что FFM API много лет разрабатывается в проекте Panama [79] с целью заменить JNI. В Java 22 [80] API выйдет из состояния preview.
Векторное API в модуле jdk.incubator.vector
[82], которое появилось ещё аж в Java 16 [83], остаётся в инкубационном статусе в шестой раз. В этом релизе лишь небольшие изменения API, исправления багов и улучшения производительности.
Векторное API останется в инкубаторе, пока необходимые фичи проекта Valhalla [84] не станут preview.
В пакете javax.crypto
появилось новое API, реализующее механизм инкапсуляции ключей [86].
Механизм инкапсуляции ключей (KEM) – это современная криптографическая техника, позволяющая обмениваться симметричными ключами, используя асимметричное шифрование. Если в традиционной технике симметричный ключ генерируется случайным образом и шифруется с помощью открытого ключа (что требует паддинга), то в KEM симметричный ключ выводится из самого открытого ключа.
В Java KEM API состоит из трёх главных классов.
KEM
[87] – входная точка API. У него есть метод getInstance()
[88], возвращающий объект KEM
для указанного алгоритма.
Encapsulator
[89] – представляет собой функцию инкапсуляции, которая вызывается отправителем. У этого класса есть метод encapsulate()
[90], который принимает открытый ключ и возвращает секретный ключ, а также key encapsulation message (которое шлётся принимающей стороне).
Decapsulator
[91] – функция декапсуляции, которая вызывается принимающей стороной. У класса есть метод decapsulate()
[92], который принимает key encapsulation message и возвращает секретный ключ. Таким образом, у обеих сторон теперь есть одинаковый симметричный ключ, с помощью которого можно дальше обмениваться данными с помощью обычного симметричного шифрования.
Пример генерации симметричного ключа и его передачи:
// Receiver side
var kpg = KeyPairGenerator.getInstance("X25519");
var kp = kpg.generateKeyPair();
// Sender side
var kem1 = KEM.getInstance("DHKEM");
var sender = kem1.newEncapsulator(kp.getPublic());
var encapsulated = sender.encapsulate();
var k1 = encapsulated.key();
// Receiver side
var kem2 = KEM.getInstance("DHKEM");
var receiver = kem2.newDecapsulator(kp.getPrivate());
var k2 = receiver.decapsulate(encapsulated.encapsulation());
assert Arrays.equals(k1.getEncoded(), k2.getEncoded());
Для KEM также добавлен интерфейс KEMSpi
[93], позволяющий предоставлять пользовательские реализации алгоритмов KEM.
В сборщик мусора ZGC, который появился в Java 15 [95], добавили поддержку поколений. Поколения в ZGC пока что отключены по умолчанию, и для их включения требуется ключ -XX:+ZGenerational
:
java -XX:+UseZGC -XX:+ZGenerational ...
В будущих версиях Java режим работы с поколениями будет по умолчанию, и ключ -XX:+ZGenerational
уже требоваться не будет.
Поколения в ZGC должны улучшить производительность Java-программ, т.к. молодые объекты, которые склонны умирать рано согласно слабой гипотезе о поколениях, будут собираться чаще, а старые объекты – более редко. При этом характеристики ZGC не должны от этого пострадать: время отклика по-прежнему должно быть сверхнизким (< 1ms) и кучи гигантских размеров (несколько терабайт) должны продолжать поддерживаться.
Напомним, что также ведётся работа [96] над поддержкой поколений в другом сборщике мусора Shenandoah [97], похожем по характеристикам на ZGC. Однако в Java 21 Generational Shenandoah попасть не успел.
Сборщиком мусора по умолчанию по-прежнему остаётся G1. Он стал дефолтным сборщиком мусора в Java 9 [98] (до него дефолтным был Parallel GC)
При динамической загрузке агентов теперь выдаётся предупреждение:
WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
Агент – это компонент, который может изменять (инструментировать) код Java-приложения во время работы. Поддержка агентов появилась в Java 5, чтобы была возможность писать продвинутые инструменты вроде профилировщиков, которым необходимо добавлять эмиссию событий в классы, или AOP-библиотек. Для включения агентов требовались опции командной строки -javaagent
или -agentlib
, поэтому все агенты тогда могли включаться только явно при старте приложения.
Однако в Java 6 появился Attach API [100], который, кроме всего прочего, позволил загружать агенты динамически прямо в работающий JVM. Благодаря этому библиотеки получили возможность подключаться к приложению и по-тихому изменять классы, не имея на то согласия от владельца приложения. Причём изменяться могут не только классы приложения, но и классы JDK. Таким образом, подвергается риску строгая инкапсуляция, которая является одним из краеугольных камней Java.
Чтобы закрыть такую потенциально опасную дыру, в Java 9 вместе с появлением модулей было предложено запретить динамическую загрузку агентов по умолчанию. Однако тогда было решено отложить на неопределённое время такое радикальное решение, чтобы дать авторам инструментов время подготовиться. В итоге, изменение дожило до наших дней, и было реализовано лишь в Java 21, но в виде предупреждения.
Чтобы подавить предупреждение, необходимо запускать JVM с опцией -XX:+EnableDynamicAgentLoading
, либо загружать агенты при старте JVM, явно перечисляя их с помощью опций -javaagent
или -agentlib
.
В будущих версиях Java планируется полностью отключить динамическую загрузку по умолчанию, и она уже не будет работать без -XX:+EnableDynamicAgentLoading
.
32-битный порт OpenJDK под Windows стал deprecated for removal. В будущем планируется избавиться от него полностью.
Удаление порта позволит ускорить разработку платформы. Также причиной стало отсутствие нативной реализации виртуальных потоков на 32-битной версии JDK 21 под Windows: виртуальные потоки в этой версии реализованы через платформенные потоки.
Полный список JEP'ов, попавших в JDK 21, начиная с JDK 17: ссылка [102].
Автор: Zheka Kozlov
Источник [103]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/oracle/387167
Ссылки в тексте:
[1] Java 21: https://openjdk.org/projects/jdk/21/
[2] 2500 закрытых задач и 15 JEP'ов: https://builds.shipilev.net/backports-monitor/release-notes-21.html
[3] здесь: http://jdk.java.net/21/release-notes
[4] здесь: https://javaalmanac.io/jdk/21/apidiff/20/
[5] как минимум 5 лет: https://www.oracle.com/java/technologies/java-se-support-roadmap.html
[6] Oracle JDK: https://www.oracle.com/java/technologies/downloads/
[7] NFTC: https://www.oracle.com/downloads/licenses/no-fee-license.html
[8] OpenJDK: http://jdk.java.net/21/
[9] GPLv2 with Classpath Exception: https://openjdk.org/legal/gplv2+ce.html
[10] Pattern Matching for switch
(JEP 441): https://openjdk.org/jeps/441
[11] Java 17: https://habr.com/ru/articles/577924/
[12] preview: https://openjdk.org/jeps/12
[13] 17: https://openjdk.org/jeps/406
[14] 18: https://openjdk.org/jeps/420
[15] 19: https://openjdk.org/jeps/427
[16] 20: https://openjdk.org/jeps/433
[17] Record Patterns (JEP 440): https://openjdk.org/jeps/440
[18] Java 19: https://openjdk.org/jeps/405
[19] String Templates (Preview) (JEP 430): https://openjdk.org/jeps/430
[20] java.lang.StringTemplate
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html
[21] java.lang.StringTemplate.Processor
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.Processor.html
[22] STR
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html#STR
[23] RAW
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html#RAW
[24] FMT
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/FormatProcessor.html#FMT
[25] java.util.Formatter
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Formatter.html
[26] process()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.Processor.html#process(java.lang.StringTemplate)
[27] Unnamed Patterns and Variables (Preview) (JEP 443): https://openjdk.org/jeps/443
[28] Unnamed Classes and Instance Main Methods (Preview) (JEP 445): https://openjdk.org/jeps/445
[29] синтетическим: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#isSynthetic()
[30] simple name: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getSimpleName()
[31] имя: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getName()
[32] Virtual Threads (JEP 444): https://openjdk.org/jeps/444
[33] Loom: https://openjdk.org/projects/loom/
[34] Java 19: https://openjdk.org/jeps/425
[35] java.lang.Thread
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html
[36] interrupt()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#interrupt()
[37] ThreadLocal
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html
[38] ForkJoinPool
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ForkJoinPool.html
[39] демоном: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#setDaemon(boolean)
[40] Thread.Builder
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.Builder.html
[41] Thread.startVirtualThread(Runnable)
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#startVirtualThread(java.lang.Runnable)
[42] Thread.isVirtual()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#isVirtual()
[43] Executors.newVirtualThreadPerTaskExecutor()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html#newVirtualThreadPerTaskExecutor()
[44] Sequenced Collections (JEP 431): https://openjdk.org/jeps/431
[45] SequencedCollection
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html
[46] SequencedSet
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedSet.html
[47] SequencedMap
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedMap.html
[48] Collection
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collection.html
[49] LinkedHashSet
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedHashSet.html
[50] List
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html
[51] SortedSet
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedSet.html
[52] Deque
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Deque.html
[53] getLast()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html#getLast()
[54] reversed()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html#reversed()
[55] LinkedHashMap
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedHashMap.html
[56] SortedMap
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedMap.html
[57] Scoped Values (Preview) (JEP 446): https://openjdk.org/jeps/446
[58] Java 20: https://openjdk.org/jeps/429
[59] инкубационном статусе: https://openjdk.org/jeps/11
[60] ScopedValue
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html
[61] set()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html#set(T)
[62] get()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html#get()
[63] where()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html#where(java.lang.ScopedValue,T)
[64] run()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.Carrier.html#run(java.lang.Runnable)
[65] get()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html#get()
[66] Structured Concurrency (Preview) (JEP 453): https://openjdk.org/jeps/453
[67] 19: https://openjdk.org/jeps/428
[68] 20: https://openjdk.org/jeps/437
[69] StructuredTaskScope
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html
[70] fork()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#fork(java.util.concurrent.Callable)
[71] join()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#join()
[72] остановлен: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#shutdown()
[73] ExecutorService
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html
[74] submit()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html#submit(java.lang.Runnable)
[75] close()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#close()
[76] Foreign Function & Memory API (Third Preview) (JEP 442): https://openjdk.org/jeps/442
[77] в Java 19: https://openjdk.org/jeps/424
[78] java.lang.foreign
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/foreign/package-summary.html
[79] Panama: https://openjdk.org/projects/panama/
[80] Java 22: https://openjdk.org/jeps/454
[81] Vector API (Sixth Incubator) (JEP 448): https://openjdk.org/jeps/448
[82] jdk.incubator.vector
: https://docs.oracle.com/en/java/javase/21/docs/api/jdk.incubator.vector/module-summary.html
[83] в Java 16: https://openjdk.org/jeps/338
[84] Valhalla: https://openjdk.org/projects/valhalla/
[85] Key Encapsulation Mechanism API (JEP 452): https://openjdk.org/jeps/452
[86] механизм инкапсуляции ключей: https://en.wikipedia.org/wiki/Key_encapsulation_mechanism
[87] KEM
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.html
[88] getInstance()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.html#getInstance(java.lang.String)
[89] Encapsulator
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Encapsulator.html
[90] encapsulate()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Encapsulator.html#encapsulate()
[91] Decapsulator
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Decapsulator.html
[92] decapsulate()
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Decapsulator.html#decapsulate(byte%5B%5D)
[93] KEMSpi
: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEMSpi.html
[94] Generational ZGC (JEP 439): https://openjdk.org/jeps/439
[95] Java 15: https://openjdk.org/jeps/377
[96] ведётся работа: https://openjdk.org/jeps/404
[97] Shenandoah: https://openjdk.org/jeps/379
[98] Java 9: https://openjdk.org/jeps/248
[99] Prepare to Disallow the Dynamic Loading of Agents (JEP 451): https://openjdk.org/jeps/451
[100] Attach API: https://docs.oracle.com/en/java/javase/21/docs/api/jdk.attach/com/sun/tools/attach/package-summary.html
[101] Deprecate the Windows 32-bit x86 Port for Removal (JEP 449): https://openjdk.org/jeps/449
[102] ссылка: https://openjdk.org/projects/jdk/21/jeps-since-jdk-17
[103] Источник: https://habr.com/ru/articles/762084/?utm_source=habrahabr&utm_medium=rss&utm_campaign=762084
Нажмите здесь для печати.