Как Docker взаимодействует с JVM? Какие есть нюансы?
Взаимодействие Docker с JVM — это набор нюансов, связанных с тем, как JVM определяет доступные ресурсы (память, CPU) в условиях ограничений cgroups контейнера.
1. Осведомлённость JVM о лимитах контейнера
До Java 10 JVM не знала о cgroups и определяла доступную память и CPU по хосту, а не по контейнеру. Это приводило к OOM-kill, когда JVM пыталась использовать больше памяти, чем разрешено контейнеру.
Начиная с Java 10+ (и бэкпорт в 8u191), флаг -XX:+UseContainerSupport включён по умолчанию, и JVM корректно определяет лимиты контейнера.
2. Настройка памяти
Пример
# Рекомендуемый подход — процент от памяти контейнера
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
Почему 75%, а не 100%? JVM использует память не только для heap:
- Heap (основная память для объектов)
- Metaspace (метаданные классов)
- Thread stacks (стек каждого потока)
- Direct memory (NIO буферы)
- Code cache (JIT-скомпилированный код)
- GC overhead
3. Настройка CPU
JVM определяет количество доступных процессоров из cgroups. Это влияет на:
- Количество потоков GC
- Размер пула
ForkJoinPool.commonPool() - Количество потоков в
parallelStream()
Пример
# Ограничение CPU
docker run --cpus=2.0 myapp
4. Graceful shutdown
Для корректного завершения Spring Boot приложения важно, чтобы Java-процесс получал сигнал SIGTERM:
Пример
# ПРАВИЛЬНО: exec-форма — java получает PID 1
ENTRYPOINT ["java", "-jar", "app.jar"]
# НЕПРАВИЛЬНО: shell-форма — SIGTERM получает shell, не java
ENTRYPOINT java -jar app.jar
В Spring Boot включите graceful shutdown:
Пример
# application.yml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
5. Время запуска
Для ускорения запуска в контейнере:
- Используйте
-Djava.security.egd=file:/dev/./urandom - Рассмотрите CDS (Class Data Sharing) или Spring AOT
- Используйте GraalVM Native Image для сокращения времени старта до миллисекунд
На собеседовании: это вопрос senior-уровня. Три ключевых нюанса: (1) UseContainerSupport — JVM должна знать о лимитах контейнера (до Java 10 не знала), (2) MaxRAMPercentage=75% — heap это не вся память JVM, (3) exec-форма ENTRYPOINT для получения SIGTERM. Знание этих деталей показывает реальный production-опыт запуска Java в Docker.