概述

最近这半个月生产环境的服务出现了一个比较奇怪的现象,相同的一个服务,有两个实例,其中一个实例的负载比较高,RPM有500-1000左右,另一个实例的RPM在20-50之间,其中低负载的实例吃的内存比较多,能吃到8G左右的内存,另一个高负载的实例,内存稳定维持在2-3个G,吃内存比较高的那个服务,运维在日常巡检的时候发现会手动重启一次服务,避免服务出现OOM的问题。因为这个问题比较频繁,所以反馈到研发这边。这个问题的经过分析定位,排除日志堆积的问题、排除是听云探针的问题之后,把原因定位在了JVM内存自动增长导致的问题,因为根据监控平台上可以看到GC的回收频次很低,因为一旦JVM可用的内存不够,如果没有设置最大可用内存,那么JVM就会自动增长,而自动增长内存后,就不会达到GC触发的条件,这样就导致内存一直增长,而没有垃圾回收的问题。

详细的分析过程

首先让运维通过利用jmap的方式将JVM的内存信息dump出来,

jmap -dump:format=b,file=文件名.dump 进程id 

经过分析发现了听云的对象占用了大概20%左右的内存空间: jvm memory cost!

因此怀疑是由于听云探针导致对象积压在内存中没有被释放,但是经过和听云的技术沟通,他们给出的反馈是JVM中的Eden区需要设置下大小,因为从监控的图表可以发现是Eden区增长过大导致(如下图),按照他们的给的建议调整之后,重新发布上线,发现JVM的内存还是会慢慢涨到8G左右。

jvm memory cost!

根据如上的尝试结果,开始怀疑是由于JVM没有设置最大内存导致的,所以在启动参数中增加了 -Xmx为4G,然后再发布生产环境观察,发现内存仍然涨到了8G左右。这说明内存的限制并没有生效。

经过如上的分析和验证,再结合我们的服务是以docker的形式部署在kubernetes环境中的,因此怀疑是由于JVM没有受到Pod的内存限制,而是以物理机器的内存作为可用内存计算的基础,比如物理机器的内存为128G,那么JVM默认最大可以占用物理内存的四分之一,那么就是32G,所以当对象开始积压在内存中之后,JVM就会扩容内存,只要没有达到JVM的最大可用内存的限制,就不会触发GC的执行。因此基于这个分析假设做了如下的调整:

  1. 将jdk版本升级到212版本(openjdk version “1.8.0_212”),因为根据网上的一些资料得知:JVM在docker中不会识别到docker的内存限制,而是识别了物理服务器的内存的大小;
  2. 在JVM的启动参数里设置了-Xmx和-Xms

经过如上的调整,再发布到开发环境,持续观察了大概2个小时左右,发现在负载维持20-50RPM的时候,内存开销一直在200-300M之间。 因此猜测这个方案可以解决这个问题。

后续进展参考 关于JVM内存泄漏调查2

参考资料

Java 8 终于支持 Docker !