Сборка и упаковка JVM: Gradle, GraalVM native и минимальные образы

Как собрать и упаковать JVM-сервис: Gradle (Kotlin DSL) vs Maven, fat-jar vs jlink-рантайм, GraalVM native image и минимальные Docker-образы — что реально влияет на старт, размер и память

JVM-сервис можно упаковать в 300-мегабайтный образ с полным JDK, а можно — в компактный native-бинарник со стартом в миллисекундах. Между этими крайностями — набор осознанных компромиссов: скорость сборки, размер, время старта, потребление памяти и совместимость библиотек.

Вводная статья серии уже показала общую картину контейнеризации Java и упомянула GraalVM native вскользь, с ориентировочными цифрами для Spring Boot и Quarkus. Здесь — тот же вопрос вглубь: механика сборки Gradle/Maven, пять разных способов упаковать рантайм одного и того же сервиса и измеренные числа по каждому — startup, пиковая память, размер образа.

Числа в этой статье получены не на Spring Boot, а на осознанно минимальном сервисе: голый com.sun.net.httpserver (только java.base + jdk.httpserver, ни одной внешней зависимости). Решение сознательное — GraalVM native-image для Spring Boot требует reflection- и resource-конфигов и AOT-обработки, а цель стенда — честно сравнить упаковку, а не бороться с чужими ограничениями. Отсюда абсолютные цифры (startup в единицы-десятки миллисекунд для native, а не 30–80 мс, как в таблице вводной статьи для полноценного Spring Boot native) меньше, чем можно ожидать по вводной статье — там мерился фреймворк, здесь — только эффект способа упаковки.

По аналогии с минимальными образами Rust и C++готовится, с 11 ноября, но с JVM-спецификой: JVM не линкуется статически, поэтому даже «минимальный» JVM-образ тянет за собой рантайм, а не только бинарник.

Сборка и упаковка JVM-приложений: GraalVM native, jlink и AppCDS

В статье

Сборка: Gradle и Maven

Maven и Gradle решают одну задачу разными моделями. Maven — декларативный XML поверх фиксированного жизненного цикла (validatecompiletestpackage → … → deploy): плагины подключаются к заранее известным фазам, порядок сборки предсказуем, но негибок. Gradle — граф задач (task graph) с явными зависимостями между ними, который сам решает, что можно пропустить или выполнить параллельно; Kotlin DSL (build.gradle.kts) добавляет типизацию и автодополнение в IDE вместо динамического Groovy.

Стенд к этой серии использует оба инструмента не ради демонстрации — так сложилось по факту: основной Maven-реактор (java-deep-dive/pom.xml) собирает большинство подмодулей, а Kotlin-часть — отдельный Gradle-модуль вне реактора, потому что Kotlin-тулчейн (плагин kotlin("jvm"), shadow-плагин) естественнее живёт в Gradle.

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <parent>
    <groupId>tech.khorost</groupId>
    <artifactId>java-deep-dive</artifactId>
    <version>0.1.0</version>
  </parent>
  <artifactId>build-packaging</artifactId>
  <packaging>jar</packaging>
  <build>
    <finalName>app</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>tech.khorost.buildpackaging.app.App</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
plugins {
    kotlin("jvm") version "2.2.0"
    application
    id("com.gradleup.shadow") version "9.5.1"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
    testImplementation(kotlin("test"))
}

kotlin {
    jvmToolchain(25)
}

tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        // Kotlin 2.2.0 не умеет target JVM 25 (проверено живьём) — явный JVM_24
        jvmTarget.set(JvmTarget.JVM_24)
    }
}

Живой нюанс тулчейна из этого же стенда: Kotlin 2.2.0 компилируется на JDK 25 (jvmToolchain(25)), но байткод-target — JVM 24, не 25. Живой лог compileKotlin честно об этом предупреждает: «Kotlin does not yet support 25 JDK target, falling back to Kotlin JVM_24 JVM target». Без явного jvmTarget.set(JvmTarget.JVM_24) Gradle 9 дополнительно падает на несогласованности между compileJava (унаследовавшим target 25 от toolchain) и compileKotlin — их приходится выравнивать вручную.

Управление зависимостями в обеих экосистемах решает одну и ту же проблему — версии транзитивных зависимостей должны совпадать по всему графу — но по-разному: Maven использует BOM (<dependencyManagement>, импорт spring-boot-dependencies и подобных), Gradle 7+ — version catalogs (libs.versions.toml) с единым источником версий для всех модулей. Порядок объявления BOM в Maven имеет значение сильнее, чем кажется: если несколько BOM пинуют одну и ту же зависимость на разные версии, побеждает первое объявление в <dependencyManagement>, а не «более специфичное» — это реальный подводный камень, встреченный в стенде статьи про data-access этой серии (Hibernate 7 требовал jakarta.persistence-api:3.2.0, а унаследованный spring-boot-dependencies BOM пинул 3.1.0; фикс — явная версия прямо на зависимости, она сильнее унаследованного BOM).

Кеширование в CI — тоже разная модель. Gradle build cache (локальный и удалённый) кеширует результаты задач по хешу входов и переиспользует их между сборками и даже между машинами при настроенном remote cache; Maven инкрементальность беднее из коробки (в основном — кеш зависимостей ~/.m2, инкрементальная компиляция ограничена). На практике для CI обоих инструментов главный рычаг — не путать кеш зависимостей с кешем Docker-слоёв: они решают разные проблемы (первый ускоряет саму сборку, второй — доставку готового артефакта), и об этом — следующий раздел.

Дальше — не про то, как собрать jar, а про то, что с ним сделать дальше внутри Docker-образа. Один и тот же байткод одного и того же сервиса упакован здесь четырьмя JVM-способами плюс GraalVM native (следующий раздел) — и первые три JVM-режима специально устроены так, чтобы разница между ними была видна на цифрах, а не только в теории.

Fat-jar — самый простой случай: один исполняемый jar с Main-Class в манифесте на стоковом eclipse-temurin:25-jre.

FROM eclipse-temurin:25-jre
WORKDIR /app
COPY target/app.jar /app/app.jar
ENV MODE=fat-jar
EXPOSE 8080
CMD ["java", "-jar", "/app/app.jar"]

Layered раскладывает тот же байткод на два COPY-слоя вместо одного jar-файла — редко меняющийся lib/ (в стенде — ResponseFormatter) отдельно от часто меняющегося app/ (HTTP-хендлеры). У Spring Boot тот же приём реализован через java -Djarmode=tools -jar app.jar extract --layers (см. Dockerfile в вводной статье серии) — здесь то же самое сделано вручную, потому что зависимостей нет и слой dependencies разводить не от чего.

FROM eclipse-temurin:25-jre
WORKDIR /app
COPY target/classes/tech/khorost/buildpackaging/lib /app/classes/tech/khorost/buildpackaging/lib
COPY target/classes/tech/khorost/buildpackaging/app /app/classes/tech/khorost/buildpackaging/app
ENV MODE=layered-jar
EXPOSE 8080
CMD ["java", "-cp", "/app/classes", "tech.khorost.buildpackaging.app.App"]

Важно понимать заранее, что именно измеряет layered-режим: в бенчмарке ниже он практически неотличим от fat-jar по startup и памяти (это ожидаемо и подтверждено измерением) — исполняется тот же байткод в том же JRE, разница в раскладке файлов на диске самой JVM не видна вообще. Смысл layered — не скорость старта, а поведение docker build/push: при правке только app/-кода пересобирается и перекачивается в registry один слой, а не весь jar целиком. Экономия — на CI-времени и трафике при частых редеплоях, а не на рантайм-профиле.

AppCDS (Application Class-Data Sharing) ускоряет именно старт JVM — за счёт того, что классы не парсятся и не верифицируются заново при каждом запуске, а подгружаются из готового архива. Стенд использует динамический AppCDS: тренировочный прогон происходит прямо на этапе сборки образа — поднимается сервис с -XX:ArchiveClassesAtExit, дожидается живого /health, получает SIGTERM, и штатное завершение JVM дампит архив классов в app.jsa, который переезжает в финальный слой.

FROM eclipse-temurin:25-jre AS trainer
WORKDIR /app
COPY target/app.jar /app/app.jar
ENV MODE=appcds-train
RUN bash -c '\
    java -XX:ArchiveClassesAtExit=/app/app.jsa -jar /app/app.jar & \
    PID=$!; \
    # ... опрос /health через /dev/tcp, затем ...
    kill $PID; wait $PID || true; \
    test -s /app/app.jsa'

FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=trainer /app/app.jar /app/app.jar
COPY --from=trainer /app/app.jsa /app/app.jsa
ENV MODE=appcds
EXPOSE 8080
CMD ["java", "-XX:SharedArchiveFile=/app/app.jsa", "-jar", "/app/app.jar"]

Тренировка на этапе сборки — не случайный выбор: продовый образ всегда несёт уже готовый архив, без двусмысленности «первый запуск медленнее, второй быстрее», которая была бы при генерации архива на лету (-XX:+AutoCreateSharedArchive). На стенде AppCDS ускоряет старт примерно на 20% (детали — в бенчмарке ниже) — эффект реальный, но скромный, потому что сервис крошечный: два класса приложения плюс jdk.httpserver. AppCDS тем ценнее, чем больше классов реально грузит приложение при старте — на «толстом» Spring Boot-сервисе с десятками автоконфигураций выигрыш был бы заметно больше, чем на этом минималистичном стенде.

jlink решает другую задачу — не startup, а размер рантайма. jdeps печатает точный список JDK-модулей, которые реально использует байткод сервиса (для этого стенда — всегда java.base,jdk.httpserver), а jlink строит урезанный JRE только из них, вместо полного eclipse-temurin:25-jre (~200 МБ).

FROM eclipse-temurin:25-jdk AS linker
WORKDIR /build
COPY target/app.jar /build/app.jar
RUN jdeps --print-module-deps --ignore-missing-deps /build/app.jar > /build/modules.txt \
 && jlink --add-modules "$(cat /build/modules.txt)" \
      --strip-debug --no-man-pages --no-header-files --compress zip-6 \
      --output /build/customjre

FROM debian:bookworm-slim
WORKDIR /app
COPY --from=linker /build/customjre /opt/customjre
COPY --from=linker /build/app.jar /app/app.jar
ENV MODE=jlink
EXPOSE 8080
CMD ["/opt/customjre/bin/java", "-jar", "/app/app.jar"]

Здесь стоит честно предупредить заранее, а не только в бенчмарке: на этом стенде jlink уменьшает образ (в разы), но увеличивает startup — и причина не в том, что custom runtime «медленный сам по себе». Диагностика через -Xlog:cds показывает разгадку прямо: стоковый eclipse-temurin:25-jre везёт дефолтный CDS-архив lib/server/classes.jsa (~14.5 МБ), которым автоматически пользуются fat/layered/appcds-режимы (все три — на том же temurin:25-jre). Custom runtime, собранный jlink, по умолчанию такого архива не содержит — JVM внутри него честно пишет в лог «Specified shared archive file not found». К этому добавляется декомпрессия сжатых --compress zip-6 модулей при загрузке классов. Итог: jlink жмёт образ, но теряет CDS и потому стартует медленнее «жирного» JRE со встроенным архивом. Лечится это не отказом от jlink, а добавлением CDS к custom runtime: плагин jlink --generate-cds-archive (доступен начиная с JDK 24 — подтверждено живьём и на JDK 25: плагин виден в выводе jlink --list-plugins как «Generate CDS archive if the runtime image supports the CDS feature», хотя в базовом jlink --help его не видно) либо отдельный AppCDS-архив поверх custom runtime, как в предыдущем режиме. В этом стенде jlink оставлен «наивным», без CDS — как поучительный контрпример: jlink сам по себе — это инструмент про размер, а не про скорость.

GraalVM native image

GraalVM native-image компилирует JVM-байткод в нативный исполняемый файл заранее (ahead-of-time), а не во время выполнения — на выходе получается один бинарник без JVM внутри. Механизм — Substrate VM: замкнутый мир классов, известный на момент сборки (closed-world assumption), собственный минималистичный рантайм и GC (в стенде — Serial GC, эргономический выбор для маленького сервиса). Именно замкнутость мира — источник главного ограничения native-image: всё, что определяется в рантайме через reflection, динамические прокси или classpath-сканирование, должно быть явно объявлено конфигами (reflect-config.json, resource-config.json и подобные) — иначе компиляция либо падает, либо собирает бинарник, который падает при первом обращении к нерасчитанному классу.

Демо-сервис стенда сознательно этого избегает: только java.base + jdk.httpserver, ноль внешних зависимостей, ноль reflection — native-image --no-fallback компилируется без единого reflect-конфига.

FROM ghcr.io/graalvm/native-image-community:25 AS builder
WORKDIR /build
COPY target/classes /build/classes
RUN native-image --no-fallback -cp /build/classes tech.khorost.buildpackaging.app.App -o /build/app

# Бинарник — динамически слинкованный ELF (glibc), не статический:
# ldd подтверждает зависимость на /lib64/ld-linux-x86-64.so.2 — нужен glibc-рантайм
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /build/app /app/app
ENV MODE=native
EXPOSE 8080
CMD ["/app/app"]

Тулчейн стенда — ghcr.io/graalvm/native-image-community:25, GraalVM CE 25.0.2+10.1, собран на JVMCI JDK 25.0.2. Образ тянется с ghcr.io без прокси, и native-image для JDK 25 собрался из коробки — fallback на альтернативный тулчейн (например, Liberica NIK) не понадобился. Цена, которую не видно в рантайм-числах, — время сборки: на этом стенде компиляция заняла примерно 25–29 секунд при пиковой памяти сборщика около 1.55 ГБ на 16 потоках — для крошечного двухклассового сервиса. Для сервиса с реальным графом зависимостей время и память сборки растут заметно, и это отдельная статья расходов, которую нужно закладывать в CI (native-сборка — не то же самое, что mvn package, и по времени, и по требованиям к раннеру).

Важная оговорка о бинарнике: он динамически линкован с glibc, не статический — ldd подтверждает зависимость на /lib64/ld-linux-x86-64.so.2. Поэтому финальный образ построен на debian:bookworm-slim, а не на scratch/distroless-static — эти базы без glibc бинарник просто не запустят. GraalVM умеет собирать и статические бинарники (флаг --static, обычно вместе с musl libc), но этот путь на стенде не проверялся — цифр по нему в этой статье нет, и специально ничего не придумано.

Бенчмарк: startup, память и размер образа на пяти режимах

Методика одна для всех пяти строк: один и тот же байткод одного и того же plain-Java сервиса (com.sun.net.httpserver, только java.base + jdk.httpserver, без внешних зависимостей — тот же стенд, что и в разделах выше), пять прогонов на режим, каждый режим — отдельный контейнер. Startup и peak RSS измеряются внутри контейнера, без публикации порта на хост:

# port-forwarder Docker Desktop на Windows добавляет к первому коннекту
# случайные 20-80 секунд — артефакт хоста, а не JVM/native. Обходится
# переносом измерения внутрь контейнера (bash /dev/tcp опрос /health).
t0=$(date +%s%N)
eval "$START_CMD & echo \$! > /tmp/svc.pid"
PID=$(cat /tmp/svc.pid)
for _ in $(seq 1 4000); do
  if exec 3<>/dev/tcp/127.0.0.1/8080 2>/dev/null; then
    printf 'GET /health HTTP/1.0\r\n\r\n' >&3
    resp=$(head -1 <&3 2>/dev/null || true)
    case "$resp" in *200*) ok=1; break;; esac
  fi
  sleep 0.002
done
t1=$(date +%s%N)
startup_ms=$(( (t1 - t0) / 1000000 ))
# peak RSS — VmHWM (историчный пик резидентной памяти) из /proc/$PID/status
rss_kb=$(awk '/VmHWM/{print $2}' "/proc/$PID/status")

Это не косметика: без переноса измерения внутрь контейнера port-forwarder Docker Desktop/Windows штрафовал первый коннект на 2–87 секунд — для JVM, стартующей за сотни миллисекунд, это полностью топит числа. Размер образа измерялся снаружи (docker image inspect, несжатый размер со слоями).

Режим startup (мс) peak RSS (МБ) размер образа (МБ)
fat (JVM fat-jar) ~150 ~65 111.4
layered (JVM) ~139 ~65 111.4
appcds (JVM+AppCDS) ~118 ~64 111.8
native (GraalVM) ~12 ~16 32.1
jlink (custom runtime) ~320 ~68 48.7
Пять способов упаковки JVM-сервиса: startup, RSS, размер образа (характерный прогон)plain-Java HTTP-сервис (java.base + jdk.httpserver), 5 прогонов на режимstartup, мс (ниже — лучше)peak RSS, МБ (ниже — лучше)размер образа, МБ (ниже — лучше)~150~139~118~12~320fatlayeredappcdsnativejlink~65~65~64~16~68fatlayeredappcdsnativejlink111.4111.4111.832.148.7fatlayeredappcdsnativejlinkfat-jar (JVM)layered (JVM)AppCDS (JVM)native (GraalVM)jlink (custom JRE)native/jlink на debian:bookworm-slim, JVM-режимы на eclipse-temurin:25-jre — часть выигрыша по размеру от разной ОС-базы, не только от упаковки

Четыре честных вывода по цифрам, по порядку убывания эффекта:

Native — самый быстрый и самый лёгкий по памяти, с большим отрывом. Порядок величины важнее точного числа: сырые значения пяти прогонов — 11, 10, 9, 17, 12 мс, разброс на глаз кажется большим (9–17 мс), но это ожидаемый шум опроса /dev/tcp в цикле с шагом 2 мс, а не нестабильность самого бинарника. Правильная формулировка — «native стартует на порядок быстрее» (единицы-десятки миллисекунд против полутора сотен у fat-jar), а не «native стартует ровно за 12 мс». По памяти разрыв тоже кратный — около 16 МБ против около 65 МБ у fat-jar, то есть примерно в 4 раза меньше.

Размер образа — не apples-to-apples сравнение, и это стоит держать в голове. Native (32.1 МБ) и jlink (48.7 МБ) заметно компактнее трёх JVM-режимов (111.4–111.8 МБ) — но не только за счёт способа упаковки как такового. native- и jlink-образы построены на debian:bookworm-slim, а fat/layered/appcds — на eclipse-temurin:25-jre, который сам по себе весит существенно больше голого Debian. Часть выигрыша размера — это разница базовых ОС-образов, а не чистый эффект AOT-компиляции или урезанного JRE. Апельсины с апельсинами здесь были бы — например — native на distroless-static (не собрано в этом стенде, бинарник динамически линкован с glibc и такую базу не запустит) против JVM-режима на минимально возможном JRE-образе того же дистрибутива. Тезис «native даёт меньший образ» в целом верен, но конкретные 3.5× в этих цифрах — комбинация двух эффектов, а не один.

AppCDS ускоряет старт примерно на 20% — умеренно, но реально: ~118 мс против ~150 мс у fat-jar, при том же RSS и почти том же размере образа (архив классов добавляет около 0.4 МБ). Скромность эффекта — не недостаток AppCDS, а следствие крошечного размера демо-сервиса: он грузит буквально пару классов приложения плюс jdk.httpserver. Чем больше классов реально парсит и верифицирует JVM при старте — а у полноценного Spring Boot-сервиса с автоконфигурациями их сотни, — тем заметнее выигрыш AppCDS.

Layered неотличим от fat по startup и RSS — и не должен отличаться. ~139 мс против ~150 мс — в пределах шума между прогонами (fat: 177/131/178/137/142, layered: 141/139/131/141/145 — диапазоны пересекаются), тот же ~65 МБ RSS, тот же 111.4 МБ образ. Исполняется тот же байткод в том же JRE — раскладка файлов на диске самой JVM не видна. Единственная реальная разница — в поведении docker build/push при частых редеплоях, не в рантайм-профиле, что и подтверждает бенчмарк.

Jlink уменьшает образ более чем вдвое, но неожиданно проигрывает по startup — 48.7 МБ против 111.4 МБ у fat-jar (в ~2.3 раза меньше), но ~320 мс против ~150 мс (заметно медленнее, а не быстрее). Причина — не сам jlink, а отсутствие CDS-архива в custom runtime по умолчанию, разобранная выше: eclipse-temurin:25-jre везёт готовый classes.jsa, jlink-рантайм — нет. Это единственный режим из пяти, где меньший размер образа куплен ценой худшего, а не лучшего startup, — и именно поэтому он в статье не «плохой пример», а поучительный: jlink закрывает вопрос размера рантайма, но не автоматически ускоряет старт, и об этом легко забыть, глядя только на размер образа.

Минимальный и надёжный образ

Из бенчмарка выше следует практическое правило: минимальный образ — это всегда комбинация двух независимых решений, «чем упаковать код» (fat/layered/appcds/jlink/native) и «на какой ОС-базе». JVM-режимы в этом стенде сидят на eclipse-temurin:25-jre — это уже минимальный JRE-образ (без компилятора, без инструментов разработки), но не distroless: в нём остаётся полноценная Debian-подобная система с shell и пакетным менеджером. native и jlink — на debian:bookworm-slim, потому что native-бинарник динамически линкован с glibc и не запустится на scratch/distroless-static; для jlink это ограничение не обязательно (custom runtime можно было бы попробовать и на более урезанной базе), но стенд держит обе базы одинаковыми ради единообразия и простоты сравнения, не ради минимума.

Практические правила, которые не зависят от режима упаковки:

  • Непривилегированный пользователь. Ни один из пяти Dockerfile в стенде не создаёт отдельного USER — это упрощение демо-стенда, не рекомендация для прод-образа. В вводной статье серии Spring Boot-пример уже показывает паттерн (addgroup/adduser/USER app) — тот же приём применим к любому из пяти режимов здесь: непривилегированный пользователь не влияет на startup/RSS/размер, но заметно сужает поверхность атаки при компрометации процесса.
  • Фиксация версий, а не плавающих тегов. eclipse-temurin:25-jre и debian:bookworm-slim — теги с движущейся точкой (патч-версии внутри тега меняются), а не иммутабельные digest. Для воспроизводимой сборки в CI и предсказуемого патчинга безопасности лучше пинить по digest (@sha256:...) там, где повторяемость важнее автоматических патчей, и обновлять его сознательно, а не полагаться на то, что :25-jre сегодня и :25-jre через месяц — один и тот же набор байт.
  • JDK на сборке, JRE/native на рантайме. Все пять Dockerfile в стенде уже следуют этому принципу через multi-stage: eclipse-temurin:25-jdk (или GraalVM-образ, или Maven-образ снаружи) участвует только в build-стадии, финальный слой не тянет за собой компилятор и инструменты разработки — они не нужны в рантайме и только увеличивают поверхность образа.

Упаковка — только один из слоёв эксплуатационной готовности; конфигурация из env, graceful shutdown под оркестратором и observability разобраны отдельно в статье «JVM в production» этой же серии.

Как выбирать

Пять режимов этой статьи закрывают разные точки на шкале «скорость сборки — startup — память — размер образа», и универсального победителя нет:

  • Fat-jar — разумный дефолт, если ничего из остального не критично: самый простой Dockerfile, самая понятная отладка, никаких сюрпризов с CDS или reflection.
  • Layered — включать, когда важен CI/CD-трафик и время docker build/push при частых редеплоях (много мелких изменений в коде на фоне стабильных зависимостей), а не рантайм-профиль — на рантайм он не влияет вовсе.
  • AppCDS — включать почти всегда, когда startup имеет значение, а reflection-heavy фреймворк не даёт применить native без серьёзной AOT-подготовки: цена (тренировочный прогон на этапе сборки) минимальна, выигрыш растёт вместе с количеством классов приложения.
  • GraalVM native — оправдан, когда startup и память — не «желательно», а требование: serverless (холодный старт биллится напрямую), CLI-утилиты, sidecar-контейнеры с жёстким лимитом памяти. Цена — время и память сборки, и совместимость: библиотеки с интенсивным reflection/динамическими прокси (полноценный Spring Boot без AOT-профиля, часть ORM) требуют явных конфигов или вовсе не соберутся с --no-fallback.
  • Jlink — стоит рассматривать, когда размер образа важнее startup (полоса пропускания registry, частая доставка образов в ограниченные по трафику окружения), и обязательно вместе с jlink --generate-cds-archive или отдельным AppCDS поверх custom runtime — иначе выигрыш в размере компенсируется проигрышем в старте, как показал бенчмарк выше.

Тот же вопрос — «минимальный и надёжный образ» — стоит на повестке и в других языках, просто с другими инструментами: сравнение подходов в C++, Rust, Java, C#, Python, Node и Scala — в статье «Сборка и минимальные образы в разных языках»Скоро.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии