Шесть лет назад я и Питер Наги задали вопрос, который был достаточно простым, чтобы быть забавным, и достаточно занудным, чтобы быть полезным: могут ли микросервисы на 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/
В нём — исходная сводная таблица по ячейкам, таблица пиковой пропускной способности, сводная таблица для графиков и таблица конфигурации рантайма.
Маленькая настройка, которая изменила результат Java
Перед основным прогоном бенчмарка я наткнулся на странный результат.
Сервис на 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
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, чтобы не пропустить свежие обновления и полезные материалы.
Комментарии (37)

ChillyVanilly
25.06.2026 15:05Вроде умный чел писал, но сплошной популизм. Сделайте не крудогонство, а ну хотя бы клон minio на java или web-сервис с терминацией SSL хотя бы, посмотрим.

Alex_RF
25.06.2026 15:05Напиши аналог minio на Go и Java и тогда сравнивай. Оба сервиса должны быть написаны на коленке для обьективного сравнения.

gohrytt
25.06.2026 15:05Все уже давно знают что если нормально работать с Java это может давать нормальный результат. ActiveJ, например, в состоянии просто взять и использовать существует года 3.
Проблема Java не в том что на нём невозможно написать хороший код, проблема в том что на нём пишут код хуже чем на других языках, плохой код наслаивается на плохой код и никто ничего не делает чтобы как-то изменить эту ситуацию.
Проблема не скорость языка, проблема скорость абстрактной фабрики абстрактных фабрик на java 8 которые пишут в среднем энтерпрайзе.
urvanov
25.06.2026 15:05абстрактной фабрики абстрактных фабрик
Это вы про AbstractSingletonProxyFactoryBean и подобные штуки?

gohrytt
25.06.2026 15:05Что-то типа такого да, я не о конкретной сущности спринга. Просто давно заметил что в мире энтерпрайза как в школе есть технари и гуманитарии и большая часть программистов на java - именно гуманитарии.
Нет больше ни одного языка от программистов на которым я бы слышал таки выражения как “выразительность композиции”, “уровень абстракции”, “экспрессивный код” и так далее.
Там где надо взять и написать функцию “Hello world!” сначала возникает класс StdoutGreeter оборачивающий функцию, потом интерфейс Greeter описывающий класс, потом билдер класса, потом фабрика интерфейса чтобы вызывать билдеры, потом обнаруживается что это всё нужно ещё вызвать, а это отдельный интерфейс Program с какими-нибудь вложенными классами и так далее.
vdudouyt
25.06.2026 15:05Это не потому, что гуманитарии, а потому, что Java зародился практически на самом плато ООП-хайпа 90-х. И без всех этих абстрактных фабрик визиторов синглтонов, если говорить чуть более современным языком, смузи с сельдереем уже не наливали.

ArtFrost
25.06.2026 15:05Ну хз, они одногодки с "древним" Delphi который уже сто раз успели закопать и некогда модного PHP. Но в коде на Delphi найти подобное это надо прямо очень заморочиться, в пыхе былых лет это тоже скорее будет задачей со звездочкой. Может конечно потом тоже к этому пришли, не знаю. Python вроде тоже не сильно с ними разошелся и я там такого не припомню.

artptr86
25.06.2026 15:05ООП по типу Java с интерфейсами, protected, final и т.п. в PHP появился только в версии 5, которая вышла в 2004 году. Но если посмотреть на код Symfony, то там как раз можно найти абстрактные фабрики, стратегии и прочее.
А Delphi — это RAD, там антипаттерн God Object в классах форм цветёт и пахнет.

HemulGM
25.06.2026 15:05А где там god object в формах?

artptr86
25.06.2026 15:05Накидал компонентов на форму, добавил обработчиков — они все в модуле формы. И получается форма, которая и данные читает/сохраняет, и бизнес-логику обрабатывает, и чего ещё только не делает.

HemulGM
25.06.2026 15:05Это никак форма не диктует. В Делфи из коробки MVVM подход.
Модуль формы - ViewModel
dfm/fmx - View
Тебя никто не заставляет писать бизнес-логику в модуле формы.
В модуле формы должны быть только связи с моделью и управление контролами. Никакой бизнес логики там быть не обязано.
God object из формы - это ваша личная проблема. Хотя, не спорю, что эта проблема у многих разработчиков.

artptr86
25.06.2026 15:05Вот у большинства модуль формы и становится god object, который делает всё. Весь код там никто писать не заставляет, но это проще и удобнее, поскольку позволяет не заморачиваться над связями объектов. Разумеется это неправильно, но порог входа низкий, вот и писали многие как попало. А в PHP было ещё хуже.

aleksandy
25.06.2026 15:05God object из формы - это ваша личная проблема.
Вообще нет. Это проблема всех, т.к. до понимания того, что форма - GodObject и это есть плохо надо ещё дорасти. А как дорасти, если в каждом первом учебнике об этом рассказывалось чуть реже, чем никогда. Все примеры - это именно когда в обработчиках накидана вся бизнес-логика. На чём учиться хорошему?

HemulGM
25.06.2026 15:05Делать всё сразу "по уму" не имеет никакого смысла. Так что если ты учишься, то пофиг как это и если у тебя получается god object, ты и сама проблему вживую ощутишь

ChillyVanilly
25.06.2026 15:05>абстрактной фабрики абстрактных фабрик
жабистов прямо заставляют это делать? кто же этот зверь?

urvanov
25.06.2026 15:05Так мы же обычно на Spring Boot пишем. Он там под капотом такого древнего Ктулху с армией приспешников из слоёв легаси поднимает, о многих чудовищах из которых уже давно все забыли.

panzerfaust
25.06.2026 15:05А это как с теорией разбитых окон. Никто не заставляет засирать улицы - а они все равно засраны.
А в джаве приходит разраб, видит лажу, воспроизводит лажу. ИИ ситуацию никак не меняет. Приходит агент, видит лажу, воспроизводит лажу. И никто особо не учит, как надо правильно. Никто не пытается разорвать порочный круг.

Alex_RF
25.06.2026 15:05На Go можно так же плохо код писать. Даже скажу точно - на Go плохо писать код значительно легче. А по поводу уменьшение производительности от наследования - судя по сравнению С и С++ - то да обычный С где-то в полтора раза быстрее С++ на GNU компиляторах по крайней мере. НО !!!! Попробуй вести большой проект на том же С и сразу захочится на С++ пересесть, потому что разрабатывать на С значительно сложнее.

XelaVopelk
25.06.2026 15:05поэтому они в сравнении и взяли Helidon SE, а не спринг. :) О том насколько распространен Helidon SE они "скромно" умолчали.

m0mus
25.06.2026 15:05Хелидон взяли потому, что Спринг бы не показал таких результатов. Хелидон веб сервер на виртуальных тредах - штука очень быстрая. Результат в пользу Java в этой статье - это заслуга Хелидона и Лейдена.

dph
25.06.2026 15:05Ну, на Go писать приличный код, пожалуй, еще сложнее и еще реже встречается, язык все-таки гораздо примитивнее, многих вещей нет или их не принято использовать.

Mes
25.06.2026 15:05Раннер бенчмарка запускает один сервис, прогоняет полную матрицу, останавливает его, затем запускает следующий сервис. Go и Java не работают одновременно, поэтому они не конкурируют друг с другом за CPU или память.
Я бы удивился, если бы автор тестировал иначе: запуская их одновременно )
SergeiMinaev
25.06.2026 15:05Ощущение, что оригинал писал ИИ - они иногда любят излишне очевидные вещи проговаривать.

Cryvage
25.06.2026 15:05Это как считать спички в коробках. Вроде посчитал, и даже какую-то разницу нашёл, но зачем?
В реальном проекте, если потребуется оптимизация или, особенно, масштабирование, это потребует архитектурных решений или переписывания алгоритма. А язык будет делом десятым. Надо выбирать язык и прочие технологии под имеющуюся команду. В чём команда лучше шарит, то и брать.

Gromilo
25.06.2026 15:05Ну хз, если держать в голове "эвристику", что java в 10 медленнее чем Go, то выбор го очевиден.
satmaelstorm
Код на JAVA рендерит JSON как конкатенацию строк.
Код на GO его честно маршаллит. Встроенной либой.
Этого достаточно, чтобы сделать выводы об объективности этого теста.
P.S. О других оптимизациях я уж и говорить не буду.
ant1free2e
сложение строк плюсиком в джаве не рекомендуется интенсивно использовать как раз из-за производительности, возможно, честный маршалинг бы еще выйграл
eee
Емнип плюсики оптимизируются жавой и переписываются на StringBuilder
artptr86
Начиная с Java 9, используется вызов makeConcatWithConstants с помощью invokedynamic: https://godbolt.org/z/938hWYWe3
А в Java 8 (и может ранее) компилятор действительно использовал StringBuilder.
ant1free2e
а invokedynamic как раз неоптимален для разовых вызовов
artptr86
Но вся та куча плюсиков из метода transform превратится в единственный вызов invokedynamic. В случае же со StringBuilder это была бы длинная цепочка append. Тем более этот вызов в хендлере двух эндпоинтов. После первого обращения закешируется.
dph
Ну, если бы в Go были строки, то можно было бы написать что-то похожее и на Go.
Но увы, есть только байтовые массивы и чуть-чуть функций сверху, поэтому эффективнее будет сделать честную маршализацию.