You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Heroku部署Spring Boot应用内存超限,求优化方案

Heroku上Kotlin/Spring Boot应用内存过高问题的优化方案

我在Heroku上部署了一个Spring Boot应用(使用Free或Hobby型dyno,内存约1GB)。问题在于应用持续占用接近内存上限,通常达到91%–92%,我不得不频繁重启dyno以维持运行。

以下是用户提供的项目build.gradle文件:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

buildscript {
    ext.kotlin_version = '1.8.0'
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-noarg:1.6.21")
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
    repositories {
        mavenCentral()
    }
}

plugins {
    id 'org.springframework.boot' version '2.7.9'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'org.jetbrains.kotlin.jvm' version '1.6.21'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
    id "org.jetbrains.kotlin.plugin.allopen" version "1.9.21"
}

apply plugin: "kotlin-jpa"
apply plugin: 'kotlin'

group = 'cl.arlanditech'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
    implementation 'org.jetbrains.kotlin:kotlin-reflect'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

    implementation 'com.auth0:java-jwt:4.3.0'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'javax.xml.bind:jaxb-api:2.3.1'
    implementation 'io.projectreactor:reactor-core:3.5.4'
    implementation "com.stripe:stripe-java:24.0.0"
    implementation 'org.postgresql:postgresql:42.6.0'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc:2.7.9'
    implementation 'com.zaxxer:HikariCP:5.1.0'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.restfb:restfb:2023.16.0'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'com.google.api-client:google-api-client:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'com.github.ben-manes.caffeine:caffeine'
    implementation 'org.apache.pdfbox:pdfbox:3.0.3'
    implementation 'org.springframework.retry:spring-retry:2.0.5'
    implementation 'org.springframework:spring-aspects'
    implementation 'org.springframework:spring-context-support'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation "com.aallam.openai:openai-client:3.8.2"
    implementation 'io.ktor:ktor-client-cio:2.3.7'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3'
    implementation 'com.resend:resend-java:3.1.0'
    implementation 'com.sendgrid:sendgrid-java:4.10.1'
    implementation 'com.google.firebase:firebase-admin:9.2.0'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'com.tinify:tinify:latest.release'
    implementation 'com.google.cloud:google-cloud-retail:2.56.0'
}

tasks.withType(KotlinCompile).configureEach {
    kotlinOptions {
        freeCompilerArgs = ['-Xjsr305=strict']
        jvmTarget = '1.8'
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

tasks.named('compileJava') {
    inputs.files(tasks.named('processResources'))
}

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

tasks.withType(KotlinJvmCompile).configureEach {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_11)
        freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
    }
}

一、Heroku上Spring Boot应用的内存优化方法

  • 调整JVM参数:Heroku的1GB内存需分配给系统进程和JVM,不能让JVM堆占满。可设置:
    JAVA_TOOL_OPTIONS="-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError"
    
    固定堆大小避免频繁扩容开销,G1GC适合中等内存场景减少GC停顿;限制元空间大小避免无限制增长;OOM时自动退出让Heroku自动重启。
  • 精简依赖:移除未使用或重复的依赖,减少类加载数量和内存占用。
  • 优化Spring Boot自动配置:关闭不需要的自动配置,比如不用Reactive功能就移除spring-boot-starter-webflux;通过@SpringBootApplication(exclude = {XXXAutoConfiguration.class})排除冗余配置。
  • 缓存配置优化:给Caffeine设置合理的最大容量和过期时间,避免缓存对象无限累积:
    spring.cache.caffeine.spec=maximumSize=1000,expireAfterAccess=10m
    
  • 数据库连接池调优:降低HikariCP的最大连接数,减少空闲连接占用内存:
    spring.datasource.hikari.maximum-pool-size=5
    spring.datasource.hikari.minimum-idle=2
    
  • 日志级别调整:降低OkHttp日志拦截器级别(从DEBUG改为INFO/WARN),避免大量请求响应数据占用内存;生产环境关闭DEBUG级日志输出。
  • 资源及时释放:对IO流、PDF文档、HTTP客户端连接等资源,使用try-with-resources确保及时关闭:
    PDDocument.load(file).use { doc ->
        // 处理PDF逻辑
    }
    

二、可能导致内存膨胀/泄漏的依赖排查

从提供的build.gradle来看,以下几点需重点关注:

  • 重复的JWT依赖:同时引入com.auth0:java-jwt和io.jsonwebtoken:jjwt,两个都是JWT处理库,保留一个即可,重复引入会增加类数量和内存开销。
  • 同时引入Web和Webflux:spring-boot-starter-web(Servlet栈)和spring-boot-starter-webflux(Reactive栈)同时存在会启动两个容器,占用双倍内存资源,根据业务场景移除其中一个。
  • 大体积SDK依赖:com.google.cloud:google-cloud-retail、com.stripe:stripe-java等第三方SDK体积较大,若仅使用少量功能,可替换为轻量HTTP客户端直接调用API,或引入SDK核心模块而非完整包。
  • PDFBox资源泄漏风险:org.apache.pdfbox:pdfbox若未正确关闭PDDocument等对象,会导致内存泄漏,需确保所有PDF操作都用try-with-resources释放资源。
  • 版本不一致的Kotlin插件:Kotlin版本存在1.8.0、1.6.21、1.9.21多个版本,版本不一致会引发类冲突,增加内存占用,需统一所有Kotlin相关插件和依赖的版本。

三、Heroku上Kotlin/Spring Boot的最佳实践

  • 统一依赖版本:将所有Kotlin相关插件、依赖的版本统一(比如全部使用1.8.0),避免版本冲突带来的额外内存开销。
  • 生产环境禁用冗余工具:确保spring-boot-devtools仅在开发环境生效,Heroku部署时不会打包该依赖(已通过developmentOnly配置实现,可再次确认)。
  • 启用内存监控:加入Spring Boot Actuator依赖,暴露内存、GC等监控端点,实时查看内存状态:
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    
    配置开放端点:
    management.endpoints.web.exposure.include=metrics,health
    management.endpoint.metrics.enabled=true
    
  • 分析GC日志:添加GC日志参数排查内存增长原因:
    JAVA_TOOL_OPTIONS="-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC -Xlog:gc*:file=/app/gc.log:time,uptime:filecount=5,filesize=10M"
    
    通过Heroku日志命令查看GC情况,判断是正常内存占用还是内存泄漏。
  • 避免不必要的反射:Kotlin的kotlin-reflect是必要的,但尽量减少不必要的反射操作,反射会增加元空间占用。
  • 使用分层JAR:Spring Boot默认的分层JAR可减少启动时的类加载开销,Heroku支持直接运行分层JAR,无需额外配置。

内容的提问来源于stack exchange,提问作者Giorgio Arlandi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:54:59