Docker容器内Java Spring Boot应用压测后内存占用无法降低问题
根因说明
- Java 8 早期版本(u191之前)默认感知不到Docker容器的cgroup资源限制,会直接按宿主机的硬件规格计算JVM内存参数、GC触发阈值,和容器实际能分配的资源不匹配,直接导致GC触发时机异常。
- JVM服务器模式默认用的Parallel GC本身内存归还策略偏保守,堆内存涨到峰值之后,只要没碰到Xmx的上限,流量低谷的时候不会主动把空闲内存还给操作系统,这和Windows环境下默认运行的客户端模式JVM内存策略完全不一样,所以会出现Windows下GC正常、容器里内存不回落的现象。
- 另外要注意:
docker stats显示的内存占用包含Linux内核维护的page cache,也就是文件读写、网络IO产生的缓存,这部分内存不属于应用进程占用,压测的时候写日志、收发包都会推高这部分数值,系统内存紧张的时候会自动回收,不用特意处理,很多人会把这部分误判成应用内存泄漏。
可直接落地的优化方案
- 把容器里的Java 8小版本升到8u191及以上,这个版本开始默认开启Docker容器资源感知,不用加额外的试验参数。如果受业务兼容性限制没法升级,就在应用启动命令里加下面这串参数,手动开启cgroup资源感知:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
- 不要依赖JVM默认的内存参数,显式自定义配置。根据容器分配的总内存上限,给堆、元空间、堆外内存设置明确配额,举个4G内存上限容器的参考配置:
-Xms512m -Xmx2g -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=512m
注意别把-Xms和-Xmx设成一样大,得给JVM留堆内存伸缩的空间,流量低的时候才能把空闲内存归还给系统。
- 把默认的Parallel GC换成G1GC,Java 8从u40版本开始G1GC就已经满足生产可用要求,比Parallel GC的空闲内存归还逻辑合理很多,加以下启动参数即可:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
- 验证效果时,配完参数重启容器再跑压测,压测结束如果等了十多分钟内存还没下降,可以通过Spring Boot Actuator的GC接口主动触发一次Full GC:先引入actuator依赖,暴露对应端点后调用
POST /actuator/gc即可。如果触发完内存明显回落,就说明不存在内存泄漏,只是GC触发时机偏晚,属于正常现象。生产环境不要频繁手动触发Full GC,会造成业务停顿。 - 要是主动触发Full GC之后内存还是没有明显下降,说明确实存在内存泄漏,压测结束后导出JVM堆快照、堆外内存统计数据,重点排查静态集合持有对象、Netty堆外内存未释放、线程池队列积压这类常见问题即可。
补充排查技巧:别光看
docker stats的数值判断应用内存占用,进容器执行jcmd <Java进程ID> VM.native_memory summary,可以看到JVM实际使用的堆、非堆、本地内存大小,直接排除page cache的统计干扰。
内容的提问来源于stack exchange,提问作者A Jesh
相关产品推荐
相关产品推荐

