Java против Go в 2026: бенчмарк через шесть лет показал другую картину
- пятница, 26 июня 2026 г. в 00:00:13
Шесть лет назад я и Питер Наги задали вопрос, который был достаточно простым, чтобы быть забавным, и достаточно занудным, чтобы быть полезным: могут ли микросервисы на Java быть такими же быстрыми, как микросервисы на Go? Речь не шла о войне языков — такие споры обычно субъективны, а хуже того, отбивают желание разбираться дальше. Практический вопрос был куда у́же: если взять небольшой HTTP-сервис, аккуратно реализовать его на Go и на Java и запустить на одном железе, окажутся ли результаты в одном диапазоне производительности?
В 2020 году ответ был «да» — для небольшой нагрузки. Тогда я заметил закономерность, которую хотел проверить снова: Java становилась интереснее по мере роста нагрузки и мощности машины. Поэтому вопрос 2026 года не «проиграл ли Go?» и не «решила ли Java все свои проблемы?». Вопрос звучит иначе: для этого сервиса, на этой машине, с текущими рантаймами — что происходит при росте нагрузки и уровня конкурентности?
Репозиторий с материалами к статье: markxnelson/go-java-go-2026. В нём код сервисов, скрипты бенчмарков, исходные результаты, сводные таблицы и скрипт построения графиков.
Для этого прогона использовались:
Go 1.26.3
Oracle JDK 26.0.1
Helidon SE 4.4.1
Linux на x86_64
Intel Xeon W-11855M, 6 ядер / 12 потоков
128 ГиБ RAM
Go-сервис использует стандартный net/http-сервер из стандартной библиотеки. Без фреймворка, без middleware-стека.
Java-сервис использует Helidon SE WebServer. Helidon 4 обрабатывает запросы через virtual threads, и health-эндпоинт подтвердил, что обработка запросов действительно шла на virtual threads.
Для Java-стороны измерялись два варианта рантайма:
Oracle JDK JVM
Oracle JDK с AOT-кэшем Leyden
Этого достаточно для данного прогона. Это удерживает статью в фокусе вопроса, который я на самом деле измерял: компактный Go-сервис против компактного Java-сервиса, оба работают последовательно на одной локальной машине.
Если вдруг решите повторить эксперимент из статьи, то удобнее всего это сделать в OpenIDE. В OpenIDE реализована первоклассная поддержка Java, Go и других самых популярных языков программирования. А поддержка Docker и 300+ плагинов в маркетплейсе доступны абсолютно бесплатно.

Оба сервиса экспоузят одинаковые эндпоинты:
GET /health GET /ready GET /api/strings/{value} GET /api/generated/{size}
Эндпоинт strings полезен для простых функциональных проверок. Эндпоинт generated — тот, который использовался в матрице бенчмарка.
Это различие важно.
В одном из ранних прогонов я тестировал 2 КБ входных данных, передавая 2-килобайтную строку прямо в URL-пути. Это в основном показало, как каждый роутер обрабатывает странный path-параметр. Возможно, интересно, но не то, что я хотел измерить. В финальном полном прогоне используется /api/generated/{size}, поэтому URL остаётся коротким, а нужный размер входных данных генерируется внутри обработчика.
Каждый запрос выполняет одинаковый небольшой объём работы:
перевод входных данных в верхний регистр
перевод входных данных в нижний регистр
разворот строки
вычисление CRC32-хэша
повторение дополнительной CRC-работы согласно WORK_FACTOR
возврат JSON с результатом и метаданными рантайма
Для бенчмарка WORK_FACTOR=10. Логирование запросов было отключено.
Это всё ещё небольшой синтетический сервис. Не корзина покупок, не антифрод-система и не платёжный API. У него нет базы данных, TLS, очереди, парсера JSON на входе и внешних зависимостей. Это сделано намеренно: цель — сделать горячий путь достаточно маленьким, чтобы было видно поведение рантайма и сервера.
Позвольте мне в этой статье использовать термин «бенчмарк» немного вольно.
Раннер бенчмарка запускает один сервис, прогоняет полную матрицу, останавливает его, затем запускает следующий сервис. Go и Java не работают одновременно, поэтому они не конкурируют друг с другом за CPU или память.
В прогоне использовались следующие параметры:
payload sizes: 7, 128, 2048, 8192 bytes concurrency levels: 1, 6, 12, 24, 48, 96, 192 repeats per cell: 2 warmup per cell: 2 seconds measurement window: 5 seconds work factor: 10
Настройки рантайма были заданы явно:
Go: GOMAXPROCS=12 GOMEMLIMIT=off Java JVM variants: -XX:ActiveProcessorCount=12 -XX:MaxRAMPercentage=75 With Leyden: -XX:+UnlockDiagnosticVMOptions -XX:-AOTRecordTraining -XX:-AOTReplayTraining
Объединённый набор результатов, использованный в статье, лежит здесь:
results/sequential_generated_leyden_feedback_full_20260608_0700432/
В нём — исходная сводная таблица по ячейкам, таблица пиковой пропускной способности, сводная таблица для графиков и таблица конфигурации рантайма.
Перед основным прогоном бенчмарка я наткнулся на странный результат.
Сервис на Helidon выглядел нормально для маленьких ответов, но при более крупных сгенерированных ответах появлялся подозрительный нижний предел задержки около 44–48 мс — когда Go-драйвер нагрузки повторно использовал персистентные HTTP/1.1-соединения. Обычный запрос через curl после прогрева такого поведения не показывал. Это было больше похоже на поведение пакетов, чем на проблему в коде приложения.
Решение было таким:
WebServer server = WebServer.builder() .port(port) .connectionOptions(socket -> socket.tcpNoDelay(true)) .routing(routing -> routing .get("/health", (req, res) -> health(res)) .get("/ready", (req, res) -> ready(res)) .get("/api/strings/{value}", (req, res) -> strings(req, res, logRequests, workFactor)) .get("/api/generated/{size}", (req, res) -> generated(req, res, logRequests, workFactor))) .build() .start();
После включения tcpNoDelay(true) кейс с 2 КБ и персистентным соединением перешёл из категории «явно сломанный бенчмарк» в категорию «нормальный сервер». Именно поэтому такие тесты стоит прогонять перед тем, как писать статью: одна пропущенная настройка способна превратиться в уверенный, но неверный вывод.
Оба сервиса также явно выставляли Content-Length для JSON-ответов известного размера.
Короткая версия: для этого сервиса, на этой машине, Java не была просто «не хуже Go». Как только тест выходил за пределы самого маленького случая, реализация на Java часто масштабировалась лучше.
В прогоне раннер использовал 10-секундный прогрев сервиса после его запуска, плюс 10-секундный прогрев перед каждой измеряемой ячейкой. Также прогонялся Leyden-replay с диагностическими опциями, отключающими запись и replay-тренировку во время измерения.
На самом маленьком сгенерированном payload все три варианта были в одном диапазоне при низкой конкурентности. С одним воркером и payload в 7 байт Go достиг около 3 200 запросов в секунду. Обычный Oracle JDK — около 2 722 запросов в секунду, а Leyden AOT — около 3 561.
Именно такой результат люди обычно запоминают из старых споров Java против Go: Go стартует быстро, код компактный, и при низкой конкурентности всё выглядит прекрасно.
Но картина менялась с ростом конкурентности.
При 192 одновременных воркерах и том же payload в 7 байт Go достиг около 59 173 запросов в секунду. Обычный Oracle JDK — около 74 044. Leyden AOT — около 99 099.
На 128 байтах Java-варианты вышли вперёд при более высокой конкурентности. При 192 воркерах Go достиг около 40 928 запросов в секунду, обычный Oracle JDK — около 62 433, Leyden AOT — около 91 124.
На 2 КБ разрыв стал больше. Пик Go — около 16 971 запроса в секунду. Пик обычного Oracle JDK — около 39 532, Leyden AOT — около 41 604.
На 8 КБ оба варианта Java заметно опережали Go в этом локальном прогоне. Пик Go — около 6 815 запросов в секунду. Пик обычного Oracle JDK — около 15 025, Leyden AOT — около 15 493.
Таблица пиковой пропускной способности из этого прогона выглядит так:

Данные с высокой конкурентностью, на которых строится основной вывод статьи:




Это и есть интересная версия истории: кривая.
Для самого маленького кейса сервисы находятся в одном диапазоне при низкой конкурентности, но Leyden AOT отрывается на высоком конце конкурентности. По мере роста сгенерированного payload преимущество Java проявляется раньше и сильнее.
Это не значит «Java быстрее Go». Это значит, что данная реализация на Java, на этом JDK, с обработкой запросов через virtual threads в Helidon и правильной настройкой сокета, масштабировалась лучше, чем данная реализация на Go в этой конкретной локальной среде.
В этом предложении много существительных. И все они нужны.
Leyden AOT не просто ускорил каждую конфигурацию запуска — но с отключёнными опциями replay-тренировки во время измерения он существенно изменил итоговый результат.
У него была лучшая пиковая пропускная способность для каждого payload в этом прогоне. На 7 байтах пик Leyden составил около 99 099 запросов в секунду при конкурентности 192, с p95 около 6,0 мс и p99 около 9,1 мс. На 128 байтах пик — около 91 124. На 2 КБ — около 41 604. На 8 КБ — около 15 493.
Это не значит, что Leyden выигрывал постоянно. Leyden AOT показал наивысшую пропускную способность в 20 из 28 вариантах payload/конкурентность, а обычный Oracle JDK JVM выиграл оставшиеся 8. Go не выиграл ни в одной из комбинаций в финальной матрице, хотя держался близко на самых маленьких кейсах с низкой конкурентностью. Общая картина пиковой пропускной способности сместилась: Leyden AOT оказался вариантом рантайма с наивысшим пиком для каждого payload в этой матрице.
Это не разочаровывает — это полезно. Leyden AOT не магический переключатель «сделать результаты бенчмарка лучше». Он меняет поведение запуска, прогрева и рантайма так, что это нужно измерять применительно к конкретной нагрузке, которая вас интересует.
Честный итог для этой статьи такой:
Leyden AOT оказался сильнейшим в разрезе пиковой пропускной способности после того, как прогон измерения аккуратнее отделил прогрев и отключил запись/replay-тренировку Leyden во время replay. Запуск и footprint всё ещё заслуживают отдельного рассмотрения.
Старый простой аргумент звучал так: Go — очевидный выбор для маленьких сетевых сервисов, потому что Java слишком тяжёлая.
Этот аргумент разбивается о результаты данной статьи.
Go остаётся прекрасным выбором для маленьких сервисов. Реализация компактна. Тулчейн прост. Стандартный HTTP-сервер вполне способен. История с деплоем единым бинарником всё ещё очень привлекательна.
Современная Java тоже отлично подходит для маленьких сервисов, и у неё совсем другой набор сильных сторон. У JVM зрелый оптимизатор, богатые инструменты наблюдаемости, отличная инженерия GC и теперь массовая модель virtual threads, которая делает блокирующий серверный код заметно дешевле, чем раньше.
Helidon SE удерживает Java-сторону достаточно компактной, чтобы это сравнение не превращалось в «минимальный Go против огромного Java-фреймворка». Это компактный Java-сервис на компактном Java-сервере.
Это не значит, что я взял бы эти цифры и сделал на их основе общекорпоративную языковую политику. Пожалуйста, не делайте так. Именно так статьи с бенчмарками превращаются в офисный фольклор, а офисный фольклор — это место, куда нюансы уходят на тихую пенсию.
Практический вывод из этой статьи такой:
Язык имеет значение, но рантайм, фреймворк, форма железа, прогрев, логирование, настройки сокета, упаковка и дизайн измерения часто значат больше, чем наши лозунги.
Пропускная способность — лишь часть истории.
Следующий проход должен добавить:
время запуска
использование RSS и heap
утилизацию CPU
GC-логи
Java Flight Recorder
async-profiler
более длинные прогоны
больше повторов на ячейку
изолированный хост для генератора нагрузки
лимиты контейнера
TLS
логирование запросов включённым и выключенным
Spring Boot
хотя бы одну настоящую зависимость, например вызов базы данных
Я бы также оставил урок с tcpNoDelay в чек-листе бенчмарка. Это не эффектно, но и быть неправым на 40 миллисекунд — тоже не эффектно.
Собрать Java-сервис:
cd helidon-service JAVA_HOME=/home/mark/jdk-26.0.1 \ PATH=/home/mark/jdk-26.0.1/bin:/home/mark/apache-maven-3.9.12/bin:$PATH \ mvn -B -DskipTests package
Запустить последовательную матрицу:
RESULTS_DIR=/home/mark/redstack/go-java-go-2026/results/sequential_generated_$(date +%Y%m%d_%H%M%S) \ GO_PORT=25081 \ JAVA_PORT=25082 \ CONCURRENCY_LEVELS="1 6 12 24 48 96 192" \ PAYLOAD_SIZES="7 128 2048 8192" \ REPEATS=2 \ DURATION=5s \ WARMUP_DURATION=2s \ JAVA_VARIANTS="oracle-jdk-jvm oracle-jdk-leyden-aot" \ WORK_FACTOR=10 \ ENDPOINT_MODE=generated \ scripts/run-sequential-matrix.sh
Раннер автоматически записывает исходные и сводные таблицы.
Оригинальная статья не закрыла этот вопрос навсегда. Она и не должна была.
Производительность — это не только свойство языка.
Это также свойство:
формы железа
версии рантайма
выбора фреймворка
прогрева
логирования
сериализации
настроек сокета
лимитов контейнера
поведения GC
дизайна драйвера нагрузки
продолжительности измерения
«шумных соседей»
частей сервиса, которые не входят в ваш бенчмарк
Это было верно в 2020-м и остаётся верным в 2026-м.
Так могут ли микросервисы на Java быть такими же быстрыми, как на Go?
Для этого сервиса, на этой машине, с этими версиями — да. И по мере роста payload и конкурентности реализация на Java часто оказывалась быстрее.
Полезный следующий вопрос «с какой формой рантайма вы хотите оперировать, наблюдать, тюнить, деплоить и жить в продакшене?». Он даёт вам что измерить, что улучшить и, в удачный день, что-то, в чём стоит поменять мнение.

Уже сейчас OpenIDE позволяет разрабатывать проекты на Java, Spring, Python, Go, PHP, JavaScript и TypeScript! А поддержка Docker и 300+ плагинов доступны абсолютно бесплатно в маркетплейсе. Пробуйте российскую IDE в деле и подписывайтесь на нас в Telegram или Max, чтобы не пропустить свежие обновления и полезные материалы.