强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Java 完全指南 / 26 - 容器化:Jib、多阶段构建、JVM 调优

26 - 容器化:Jib、多阶段构建、JVM 调优

基础镜像选择

镜像大小包含内容适用场景
eclipse-temurin:21-jdk-alpine~180MBJDK + Alpine构建阶段
eclipse-temurin:21-jre-alpine~80MBJRE + Alpine运行推荐
eclipse-temurin:21-jre~200MBJRE + Ubuntu需要 glibc
gcr.io/distroless/java21-debian12~50MB最小运行时生产推荐
amazoncorretto:21-alpine~130MBAmazon CorrettoAWS 环境

💡 生产环境推荐 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 优势

特性JibDockerfile
需要 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启动所有服务

⚠️ 注意事项

  1. 基础镜像选择 — Alpine 最小但 musl libc 可能有兼容问题;考虑 distroless
  2. 不要以 root 运行 — 安全最佳实践。
  3. 层缓存优化 — 依赖层和代码层分开,先 COPY 依赖声明文件。
  4. 镜像扫描 — 使用 docker scan 或 Trivy 检查已知漏洞。
  5. 不要在镜像中存储敏感信息 — 使用环境变量或 Docker Secrets。

💡 技巧

  1. Jib 分层策略 — 自动分为依赖层、快照层、资源层、类层,修改代码只重建最后一层。
  2. Spring Boot 分层 JARjava -Djarmode=layertools -jar app.jar extract 分出四层。
  3. 多架构构建docker buildx build --platform linux/amd64,linux/arm64 .
  4. 镜像版本策略latest + 语义化版本号 + Git commit SHA。

🏢 业务场景

  • Kubernetes 部署: 容器化是 K8s 的前提,每个微服务独立镜像。
  • CI/CD 流水线: Jib 无需 Docker daemon,适合 Jenkins/GitHub Actions。
  • 本地开发: Docker Compose 一键启动所有依赖(MySQL、Redis、Kafka)。
  • 蓝绿部署: 新旧版本镜像切换,零停机发布。

📖 扩展阅读