JavaSeniorExperience

Представьте, сервис на Java стал медленнее или нестабильнее после релиза. Какие language/runtime-specific причины вы проверите?

Проверяют JIT-прогрев, давление на GC (heap dump + GC-логи), thread contention (jstack/jcmd), изменение флагов JVM, рост Metaspace и регрессии в зависимостях.

Диагностика деградации производительности Java-сервиса после релиза

Когда сервис начинает тормозить или нестабильно работать именно после релиза, первым делом важно отделить регрессию в коде от изменения нагрузки. Ниже — систематический список Java/JVM-специфичных причин, которые стоит проверить в первую очередь.

1. JIT-компиляция и прогрев (Warm-up)

После перезапуска JVM начинает интерпретировать байткод и только постепенно JIT-компилирует горячие методы. Если метрики сняты сразу после деплоя, они покажут деградацию, которая исчезнет через 5–15 минут. Проверяйте графики с разбивкой по времени жизни пода.

Если прогрев стал длиннее, чем раньше, — возможно, в новом коде появились новые горячие пути или методы стали полиморфными (один интерфейс — много реализаций), что мешает инлайнингу.

2. Утечка памяти и давление на GC

Самая частая причина — новый код создаёт больше объектов или удерживает ссылки дольше, чем нужно. Симптомы: растущий heap, учащающиеся Full GC, длинные паузы STW (Stop-The-World).

Диагностика:

  • Включить GC-логирование: -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m
  • Снять heap dump: jcmd <pid> GC.heap_dump /tmp/heap.hprof и открыть в Eclipse MAT или VisualVM
  • Проверить метрику jvm_gc_pause_seconds в Prometheus

3. Изменение политики или алгоритма GC

Если в новом релизе обновили версию JDK (например, с 17 на 21), или изменили флаги запуска, алгоритм GC мог поменяться. G1 стал дефолтом с Java 9, ZGC и Shenandoah — опциональные низколатентные коллекторы. Убедитесь, что флаги в docker-образе совпадают с желаемыми:

java -XX:+UseZGC -Xms512m -Xmx2g -jar app.jar
# или для G1
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar

4. Блокировки и contention

Новый код мог ввести синхронизацию там, где её раньше не было: synchronized-блок, ReentrantLock, или неосторожное использование Collections.synchronizedMap вместо ConcurrentHashMap. Под нагрузкой это превращается в узкое горло.

Диагностика через thread dump:

jcmd <pid> Thread.print > /tmp/threads.txt
# или через jstack
jstack <pid> | grep -A5 "BLOCKED"

В Prometheus смотрите jvm_threads_states_threads{state="blocked"}.

5. ClassLoader и метаспейс

Активное использование рефлексии, Proxy, Groovy/scripting или фреймворков, генерирующих классы в runtime (например, Hibernate Proxy), может привести к росту Metaspace. Если Metaspace переполняется, JVM выполняет Full GC для его очистки.

Симптом: OutOfMemoryError: Metaspace или резкие Full GC при стабильном heap.

java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar app.jar

6. Компиляция с другими флагами оптимизации

Убедитесь, что production-сборка не компилируется с -g (debug info) или без -O. Также проверьте, не включён ли агент наподобие Jacoco или AspectJ в production-образе — они серьёзно замедляют выполнение.

7. Проблемы с профилем загрузки классов (CDS / AppCDS)

Если проект использует Class Data Sharing (-Xshare:on), устаревший archive после релиза может вызывать падения или игнорироваться JVM, лишая выигрыша в старте.

8. Регрессия в зависимостях

Обновлённая версия библиотеки (Jackson, Hibernate, Netty) может иметь регрессию производительности. Сравните pom.xml или build.gradle между релизами. Используйте mvn dependency:tree для проверки транзитивных зависимостей.

Практический чеклист диагностики

  • Сравнить GC-логи до и после релиза: частота Full GC, паузы STW
  • Снять профиль CPU через async-profiler: ./profiler.sh -d 30 -f /tmp/profile.html <pid>
  • Сравнить thread dumps под нагрузкой: искать BLOCKED/WAITING потоки
  • Проверить heap dump: топ объектов по retained size
  • Сверить JVM-флаги в новом и старом образе
  • Проверить агенты и javaagent-параметры в Dockerfile/Helm

Подводные камни

  • Снятие метрик сразу после старта даёт ложную деградацию из-за отсутствия JIT-прогрева — дождитесь нескольких минут стабильной нагрузки.
  • Heap dump под нагрузкой временно останавливает JVM — не снимайте его на единственном экземпляре в prod.
  • async-profiler требует perf_events или -XX:+PreserveFramePointer — без этого CPU-профиль будет неполным.
  • Увеличение MaxMetaspaceSize без ограничения может привести к OOM на уровне ОС, а не JVM.
  • Переход с Serial/Parallel GC на G1 при той же настройке heap может увеличить паузы в случае небольшого heap (<1GB).
  • Synchronized-блок на this — распространённая ловушка: если объект используется в разных контекстах, блокировка распространяется шире, чем ожидалось.
  • Transitive dependency bump (например, logback через Spring) может незаметно изменить поведение сериализации/логирования.
  • AppCDS archive привязан к конкретному JAR-файлу; при смене артефакта его нужно перегенерировать.

What hurts your answer

  • Сразу обвинять Java, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг Java
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics

Представьте, сервис на Java стал медленнее или нестабильнее после релиза. Какие language/runtime-specific причины вы проверите? | Talanto