В прошлой заметке я показал, как код Kotlin случайно нацелился на новый Java API, когда JDK в сборке был обновлен до 21. Эту проблему можно решить с помощью флага -Xjdk-release
компилятора Kotlin, или используя набор инструментов (toolchain) Gradle для сборки со старым JDK.
Если вы читаете документацию Gradle…
Использование инструментальных цепочек (toolchain) в Java — предпочтительный способ привязки к версии языка
…или документацию по Android…
Мы рекомендуем всегда указывать цепочку инструментов Java.
…вас никто не осудит за то, что вы считаете, что цепочки инструментов Java — это лучший способ!
Однако цепочки инструментов Java редко бывают хорошей идеей. Давайте разберемся, почему.
Плохая документирование
На прошлой неделе я выпустил новую версию Retrofit, которая использует инструментальную цепочку Java для работы с Java 8. Инструментальные цепочки были внесены некоторое время назад, и я просто забыл удалить их. Как следствие, Javadoc был создан с использованием JDK 8 и поэтому не доступен для поиска. Javadoc с возможностью поиска появился в JEP 225 с JDK 9.
Следующий выпуск Retrofit будет сделан без тулчейна и с последней версией JDK. Его документация будет иметь все достижения Javadoc за последние 10 лет, включая поиск и более современный HTML/CSS.
Незнание ресурсов
Старые JVM были печально известны тем, что не обращали внимания на ограничения ресурсов, накладываемые системой. Рост контейнеров, особенно в CI-системах, означает, что ограничения ресурсов вашего процесса отличаются от ограничений хост-операционной системы. В JDK 10 появилась поддержка cgroups, а в JDK 15 она была расширена до cgroups2.
Оба эти изменения были перенесены в ветки 8 и 11, но поскольку цепочки инструментов Gradle будут использовать уже установленный JDK, если он доступен, вам необходимо обновить JDK 8 и/или JDK 11. Вы обновили?
Не хочу далеко отходить от темы, но если вы установили его с помощью SDKMAN! или подобных инструментов управления JDK, то велика вероятность, что он сильно устарел. Я поддерживаю все свои JDK в актуальном состоянии, устанавливая их через Homebrew tap, который сам автоматически обновляется с помощью Azul Zulu API. Пока я часто делаю brew upgrade
, каждый основной релиз JDK, который у меня установлен, будет обновлен.
Без тулчейнов Java современный JDK (даже устаревший патч релиз) будет соблюдать ограничения на ресурсы и работать гораздо лучше в контейнерных средах.
Ошибки компилятора
В любом программном обеспечении есть ошибки, а иногда они есть в JVM, компиляторе Java или в них обоих. Если вы используете версию JVM и компилятора Java 10-летней давности, вы гораздо больше рискуете столкнуться с ошибками компилятора, особенно если речь идет о функциях, появившихся ближе к выпуску релиза.
Было много проблем с компиляцией лямбд, которые появились в Java 8. Если вы используете компилятор Java из JDK 8 для работы с JVM Java 8, вы все равно можете столкнуться с этими ошибками. Даже если вы поддерживаете JDK 8 в актуальном состоянии, многие исправления не переносятся обратно. Вы можете найти их на трекере проблем без особых усилий.
Является ли компилятор Java в JDK 22 полностью свободным от ошибок? Нет. Но является ли использование компилятора Java из JDK 22 для исходников, ориентированных на Java 8 и использующих только возможности языка Java 8, намного безопаснее, чем использование компилятора из JDK 8? Безусловно.
Худшая производительность
Oracle и другие крупные компании, занимающиеся разработкой JVM, тратят много человеко-часов на то, чтобы сделать JVM быстрее. У нас есть более новые сборщики мусора, которые используют меньше памяти и потребляют меньше ресурсов процессора. Работа, которая выполнялась при запуске, откладывается до первого использования, чтобы попытаться распределить затраты на время жизни процесса. Алгоритмы и представления в памяти специализируются для общих случаев.
Компилятор языка — это, по сути, наихудший сценарий для JVM. Бесконечные манипуляции со строками, создание объектов и множество мапов. За прошедшие годы эти области получили множество улучшений. Мое любимое из них — то, что строки, основанные на ASCII, в Java 9 внезапно занимают вдвое меньше памяти, чем в Java 8. А знаете, что часто бывает полностью ASCII? Исходный код Java и Kotlin!
Не требуется для кросс-компиляции
Используя компилятор Java из JDK 8, я могу установить -source
и -target
на «1.7», чтобы скомпилировать класс, работающий на Java 7 JVM. Однако это не мешает мне использовать API-интерфейсы Java 8. Необходимо добавить в -bootclasspath
указатель на среду выполнения JDK 7 (rt.jar
), чтобы компилятор знал, какие API доступны в Java 7. В качестве альтернативы можно использовать инструмент вроде Animal Sniffer, чтобы убедиться, что не используются API новее Java 7. В этом мире просто компилировать с JDK 7, чтобы использовать Java 7, может быть проще.
Однако в JDK 9 все изменилось. Компилятор теперь содержит запись всех публичных API из каждой версии Java, начиная с Java 8. Это также позволяет указать единственный флаг компилятора, --release
, который устанавливает версию языка исходного кода, версию целевого байткода и доступные API рантайма в соответствии с указанным выпуском. Компилировать с помощью старого JDK, чтобы использовать старую JVM, больше не имеет смысла.
Нерациональное использование дискового пространства
Все эти JDK занимают ненужное место в вашем домашнем каталоге. Каждый JDK занимает несколько сотен мегабайт. По умолчанию Gradle пытается подобрать JDK, когда запрашивается цепочка инструментов. Владельцы проектов могут указать дополнительные атрибуты, такие как поставщик JDK, которые могут привести к тому, что существующие JDK не будут им соответствовать. Это означает, что даже если один проект заставляет вас установить Eclipse Temurin JDK 8, другой может заставить использоват Azul Zulu JDK 8. Так что теперь у вас не только куча старых JDK, но и две или три копии каждого. Мой кэш JDK в ~/.gradle
составляет почти 2 ГБ.
JVM не принадлежит Gradle
Инструментальные цепочки используются только для задач, которые создают новую JVM. Это означает компиляцию (Java или Kotlin) и запуск юнит-тестов. Они не управляют JVM, которая используется для запуска фактической сборки Gradle или каких-либо плагинов в ней. Если у вас есть минимальные требования там или в других инструментах на базе JVM, которые вызываются сборкой Gradle, цепочка инструментов вам не поможет.
Если ваша сборка уже имеет минимальные требования к JDK, то зачем принудительно устанавливать старый JDK, если новый уже доступен на диске, отлично кросс-компилируется, имеет меньше ошибок в компиляторе, быстрее собирается и эффективнее соблюдает ограничения системного процессора и памяти?
Не все так плохо
Я хочу подчеркнуть, что цепочки инструментов однозначно не являются хорошей идеей для компиляции. Однако в других областях они все же полезны.
Retrofit имеет runtime поведение, которое меняется в зависимости от версии JVM, на которой он запущен. (Это происходит потому, что до Java 16 требовались различные хаки для поддержки вызова методов по умолчанию через Proxy
). Этот код необходимо протестировать на разных версиях JVM. В результате мы компилируем на последней версии Java, но на задаче Test тестируем на самой младшей из поддерживаемых Java с помощью инструментальных цепочек. Не нужно беспокоиться о том, что у пользователя есть странные старые JDK для Java 14, потому что теперь она устанавливается по требованию при запуске полного набора тестов.
Некоторые инструменты, которые погружаются во внутреннее устройство JDK, регулярно ломаются на новых версиях компилятора, потому что они полагаются на нестабильные API. Я имею в виду такие вещи, как Google Java Format или Error-Prone. Не нужно мешать остальным частям вашего проекта пользоваться последними версиями JDK, если эти инструменты запускаются через задачу JavaExec
, вы можете использовать цепочку инструментов, чтобы держать их на более старом JDK, пока не появится новая версия.
Что делать?
Используйте флаг --release
, если вы компилируете Java! Теперь Gradle предоставляет свойство для этого.
Используйте флаг -Xjdk-release
, если вы компилируете Kotlin. В будущих версиях плагина Kotlin Gradle откроет для него хорошее DSL-свойство.
Если вы ориентируетесь на Android (с Java, Kotlin или обоими), вам нужно указать только sourceCompatibility
(для Java) и jvmTarget
(для Kotlin). TargetCompatibility
не нужен, так как по умолчанию он будет соответствовать sourceCompatibility
.
Что бы вам ни говорили в документации Gradle или Android, не используйте цепочки инструментов! Оставьте цепочки инструментов для юнит-тестов JVM или несовместимых инструментов.