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堆占满。可设置:
固定堆大小避免频繁扩容开销,G1GC适合中等内存场景减少GC停顿;限制元空间大小避免无限制增长;OOM时自动退出让Heroku自动重启。JAVA_TOOL_OPTIONS="-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError" - 精简依赖:移除未使用或重复的依赖,减少类加载数量和内存占用。
- 优化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日志参数排查内存增长原因:
通过Heroku日志命令查看GC情况,判断是正常内存占用还是内存泄漏。JAVA_TOOL_OPTIONS="-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC -Xlog:gc*:file=/app/gc.log:time,uptime:filecount=5,filesize=10M" - 避免不必要的反射:Kotlin的
kotlin-reflect是必要的,但尽量减少不必要的反射操作,反射会增加元空间占用。 - 使用分层JAR:Spring Boot默认的分层JAR可减少启动时的类加载开销,Heroku支持直接运行分层JAR,无需额外配置。
内容的提问来源于stack exchange,提问作者Giorgio Arlandi
相关产品推荐
相关产品推荐

