JDK8 G1 堆内存居然不释放
背景
k8s环境中有个pod,pod里跑了一个java进程。 采用的是jdk版本是openjdk version "1.8.0_342"。 启动时有这些jvm参数:
-server
-Dspring.datasource.druid.maxActive=80
-XX:MaxGCPauseMillis=200
-XX:+UseG1GC
-XX:+UseContainerSupport
-XX:MinRAMPercentage=20.0
-XX:MaxRAMPercentage=70.0
-Djava.security.egd=file:/dev/./urandom
-Duser.timezone=GMT+08
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/mnt/log
-XX:+UnlockExperimentalVMOptions
depoyment的yaml文件中限制的内存是4g.
现在发现这个deployment的5个pod内存都占用了90%以上,且持续不降,部分pod还重启过3/4次

排查过程
怀疑存在内存泄漏或内存溢出
于是进入pod把堆dump下来,下载到本地分析。(由于是客户环境,没有权限直接操作k8s,只能访问dashborad面板,所以下载文件这个过程也是挺折腾的)
dump下来的堆文件确只有不到2g样子(有点纳闷,但没深究)
采用与博文 线上FullGC频繁的排查 排查方式分析堆文件,一无所获。
查看gc情况

可以看到,青年代发生了1.3万次gc,老年代没有发生过gc. (此时离pod启用有9天时间)
同时也能看到内存分配和使用情况:
- 堆内存: 使用了1.6g, committed2.8g内存(可以理解为jvm向操作系统申请了2.8g内存),最大堆内存限制为2.8g. (这个2.8就是deployment的4MaxRAMPercentage参数,也就是40.7=2.8)
- 非堆内存:包括直接内存、metaspace 、code cache 等。 使用了400m不到,committed400m内存。
然后很好奇的手动执行了一次fullgc,再看jvm情况,如下:

然后看k8s的dashboard

注意:因为这篇博文是事后写的,上面的图有些并不是同一个pod的信息,所以有些信息会对不上,但不影响问题分析。
可以看到堆内存的used和committed下降了,committed下降了大概1.3g样子,刚好就是dashboard上显示的内存差异,非堆内存几乎没有下降。
执行了fullgc后第二天,bashboard内存占用基本没有变动,没有再升上去
可以分析得出一个基本结论:
- 问题在堆内存,且代码可以基本排除内存泄漏或溢出的问题
- 青年代的gc 没有退还内存给操作系统,fullgc后退还了1.3g样子给操作系统
查资料,尝试增加jvm参数:
-XX:MaxHeapFreeRatio=30
-XX:MinHeapFreeRatio=10
-XX:MaxHeapFreeRatio=30
最大堆内存空闲比例,大致理解为jvm向操作系统了申请了2g内存,gc后只使用了1g. 那么空闲了1g,空闲比例就是50%,大于了30%,于是就会退还内存给操作系统。
-XX:MinHeapFreeRatio=10
同理,最小堆内存空闲比例,如果gc空闲比例小于了这个值,jvm就会向操作系统申请更多的内存
后面也尝试增加过其他参数
观察一段时间后,发现青年代gc后,还是没有减少committed内存。也没有增加老年代的gc次数
解决
其实算不上解决,或者解决得不够优雅。考虑到切换jdk版本,或者修改垃圾收集器可能产生意料之外的问题和成本,所以采用:
- 周期性触发一次fullgc. 比如通过sidecar,或者linux的crontab或者代码定时任务等。
后文
后面再继续查资料,搜到一个类似问题的网址:
how-to-reduce-committed-heap-memory-in-jvm

系统推荐
- JVM垃圾收集器
- NGINX
- 提取Docker镜像中的文件
- MongoDB高可用
- 你真的会拼接字符串吗?
- 正则表达式匹配第几个符号问题
- Spring RetryTemplate
- Cornell Notes System
- ShadowsockServerUpdatePort
- SQL优化
- MySQL数据迁移到PGSQL
- 分布式问题
- 随机毒鸡汤:说啥啥不听,干啥啥不行,吃啥啥不剩。