Java 完全指南 / 26 - 容器化:Jib、多阶段构建、JVM 调优
26 - 容器化:Jib、多阶段构建、JVM 调优
基础镜像选择
| 镜像 | 大小 | 包含内容 | 适用场景 |
|---|
eclipse-temurin:21-jdk-alpine | ~180MB | JDK + Alpine | 构建阶段 |
eclipse-temurin:21-jre-alpine | ~80MB | JRE + Alpine | 运行推荐 |
eclipse-temurin:21-jre | ~200MB | JRE + Ubuntu | 需要 glibc |
gcr.io/distroless/java21-debian12 | ~50MB | 最小运行时 | 生产推荐 |
amazoncorretto:21-alpine | ~130MB | Amazon Corretto | AWS 环境 |
💡 生产环境推荐 distroless — 体积最小,无 shell,安全性最高。
Dockerfile 多阶段构建
# ---- 阶段1:构建 ----
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# 利用 Docker 层缓存 —— 先复制依赖声明,再复制源码
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
# 编译打包
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# ---- 阶段2:Spring Boot 分层构建 ----
FROM eclipse-temurin:21-jre-alpine AS layered
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# ---- 阶段3:最终运行镜像 ----
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 非 root 用户(安全最佳实践)
RUN addgroup -S app && adduser -S app -G app
# Spring Boot 分层 —— 每层独立缓存
COPY --from=layered /app/dependencies/ ./
COPY --from=layered /app/spring-boot-loader/ ./
COPY --from=layered /app/snapshot-dependencies/ ./
COPY --from=layered /app/application/ ./
USER app
# JVM 容器感知参数
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+UseZGC \
-XX:+ZGenerational \
-Djava.security.egd=file:/dev/./urandom"
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
构建与运行:
# 构建镜像
docker build -t myapp:1.0 .
# 运行容器
docker run -d \
-p 8080:8080 \
-e JAVA_OPTS="-Xmx512m" \
-e SPRING_PROFILES_ACTIVE=prod \
--name myapp \
myapp:1.0
# 查看日志
docker logs -f myapp
# 进入容器调试(非 distroless)
docker exec -it myapp sh
# 资源限制
docker run -d \
--memory=1g \
--cpus=2 \
-p 8080:8080 \
myapp:1.0
Maven 版本的 Dockerfile
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
COPY src/ src/
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Google Jib(无需 Dockerfile)
Maven 配置
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:21-jre-alpine</image>
</from>
<to>
<image>registry.example.com/myapp</image>
<tags>
<tag>${project.version}</tag>
<tag>latest</tag>
</tags>
</to>
<container>
<jvmFlags>
<jvmFlag>-XX:+UseContainerSupport</jvmFlag>
<jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag>
<jvmFlag>-XX:+UseZGC</jvmFlag>
</jvmFlags>
<ports>
<port>8080</port>
</ports>
<user>1000</user>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
<pluginExtensions>
<pluginExtension>
<implementation>com.google.cloud.tools.jib.gradle.extension.approot.AppRootPluginExtension</implementation>
</pluginExtension>
</pluginExtensions>
</configuration>
</plugin>
Gradle 配置
jib {
from {
image = "eclipse-temurin:21-jre-alpine"
}
to {
image = "registry.example.com/myapp"
tags = setOf(project.version.toString(), "latest")
}
container {
jvmFlags = listOf(
"-XX:+UseContainerSupport",
"-XX:MaxRAMPercentage=75.0",
"-XX:+UseZGC"
)
ports = listOf("8080")
user = "1000"
}
}
mvn jib:dockerBuild # 构建到本地 Docker daemon
mvn jib:build # 推送到远程镜像仓库
mvn jib:buildTar # 导出为 tar 文件
Jib 优势
| 特性 | Jib | Dockerfile |
|---|
| 需要 Docker daemon | ❌ 不需要 | ✅ 需要 |
| 分层策略 | 自动优化 | 手动控制 |
| CI 友好 | ✅ 无需 Docker-in-Docker | 需要 |
| 构建速度 | 快(增量) | 中等 |
| 跨平台 | ✅ | 需要 buildx |
JVM 容器感知
# JDK 10+ 自动感知容器资源限制
-XX:+UseContainerSupport # 默认开启
-XX:MaxRAMPercentage=75.0 # 最大堆占容器内存的 75%
-XX:InitialRAMPercentage=50.0 # 初始堆大小
# 验证 JVM 是否正确感知容器资源
java -XX:+PrintFlagsFinal -version | grep -E "MaxRAM|ActiveProcessor"
容器 JVM 参数推荐
| 容器内存 | 推荐参数 | 说明 |
|---|
| 512MB | -Xmx384m -XX:MaxRAMPercentage=75 | 留 25% 给非堆 |
| 1GB | -Xmx768m -XX:MaxRAMPercentage=75 | 一般 Web 应用 |
| 2GB | -Xmx1536m -XX:MaxRAMPercentage=75 | 中型应用 |
| 4GB+ | -XX:MaxRAMPercentage=75 | 大型应用 |
⚠️ 容器内存不足时 Linux OOM Killer 会直接杀死进程,不会触发 OutOfMemoryError。
Docker Compose 本地开发
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
depends_on:
mysql:
condition: service_healthy
kafka:
condition: service_started
volumes:
- ./logs:/app/logs
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mydb
volumes:
- mysql-data:/var/lib/mysql
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
kafka:
image: confluentinc/cp-kafka:7.5.0
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
volumes:
mysql-data:
镜像大小优化技巧
# 1. Alpine 基础镜像
FROM eclipse-temurin:21-jre-alpine
# 2. 合并 RUN 指令减少层数
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
# 3. 使用 .dockerignore
# 文件: .dockerignore
# .git
# .idea
# build/
# target/
# *.log
# node_modules/
# 4. 使用 distroless(无 shell,最小攻击面)
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/build/libs/*.jar /app.jar
ENTRYPOINT ["/app.jar"]
常用 Docker 命令
| 命令 | 说明 |
|---|
docker build -t name:tag . | 构建镜像 |
docker run -d -p 8080:8080 name | 运行容器 |
docker logs -f container | 查看日志 |
docker exec -it container sh | 进入容器 |
docker stats | 资源使用统计 |
docker system prune -a | 清理未使用资源 |
docker scan name:tag | 安全扫描 |
docker compose up -d | 启动所有服务 |
⚠️ 注意事项
- 基础镜像选择 — Alpine 最小但 musl libc 可能有兼容问题;考虑
distroless。 - 不要以 root 运行 — 安全最佳实践。
- 层缓存优化 — 依赖层和代码层分开,先 COPY 依赖声明文件。
- 镜像扫描 — 使用
docker scan 或 Trivy 检查已知漏洞。 - 不要在镜像中存储敏感信息 — 使用环境变量或 Docker Secrets。
💡 技巧
- Jib 分层策略 — 自动分为依赖层、快照层、资源层、类层,修改代码只重建最后一层。
- Spring Boot 分层 JAR —
java -Djarmode=layertools -jar app.jar extract 分出四层。 - 多架构构建 —
docker buildx build --platform linux/amd64,linux/arm64 . - 镜像版本策略 —
latest + 语义化版本号 + Git commit SHA。
🏢 业务场景
- Kubernetes 部署: 容器化是 K8s 的前提,每个微服务独立镜像。
- CI/CD 流水线: Jib 无需 Docker daemon,适合 Jenkins/GitHub Actions。
- 本地开发: Docker Compose 一键启动所有依赖(MySQL、Redis、Kafka)。
- 蓝绿部署: 新旧版本镜像切换,零停机发布。
📖 扩展阅读