一、问题背景

        技术支持反馈生产环境数据显示异常,并且报警ECS cpu使用率超过阈值,登录生产服务器后,使用top指令发现,当前cpu占用率已达190%(2核),使用 top -Hp + jsatck ,定位出cpu占用最高的线程为GC线程。

结合查询业务错误日志,可以断定是内存溢出,导致GC线程在不停进行垃圾回收,但又始终回收不了内存,cpu自然而然就上去下不来了。

通过指令 jmap -dump:live,format=b,file=/data/dump.hprof [pid] 导出当前内存堆栈信息后,将服务重启,以免影响用户正常使用。

二、问题发现

将导出的dump文件导入MAT(MemoryAnalyzer)中进行分析,打开后软件提示存在内存泄漏,点开后可发现PoolingHttpClientConnectionManager 这个对象是被一个数组对象引用着的,数组内对象数量多达38万,占据了总内存的96%

三、代码定位

        从MAT中可以看到,最终的对象是 com.aliyun.oss.common.comm.IdleConnectionReaper,打开源码可以看到,该类中确实存在一个静态的数组对象,并且提供了方法去往数组中添加 HttpClientConnectionManager 对象

        再往上查找在什么时候调用这个register方法,发现是在 DefaultServiceClient 的一个创建方法中会去调用(config的useReaper属性可自行查看源码,默认值为true)

        接着往上可以看到这个create操作是在 DefaultServiceClient 构造器中调用的。

        往上可定位到入口位置为ossclient的构造器。

        最后在看下在实际代码中OssClient是如何创建使用的。

        所以为什么connectionManagers列表会持有这么多对象,是因为我们每调用一次getOSSClient()就会生成一个OssClient对象,就会在connectionManagers添加一个PoolingHttpClientConnectionManager对象。

四、问题重现

        本地ideal模拟,首先将JVM环境参数,调整为 -Xms80M -Xmx80M -Xss256K,接着写一个测试Controller去不断创建ossClient对象,并观察JVM情况。

        使用postman不断循环去调用这个方法。可以connectionManagers数组数量在不断上涨,以及最终看到内存溢出的现象。

        从VirtualGC中也可以看到,循环期间old区内存在不停上涨,最终不断进行fullGC后也不会减少,直至占完所有空间。 

五、解决方案

1. 每次调用完需要在代码中,显式调用OssClient.shutdown方法

底层会去触发调用DefaultServiceClient 中的shutdown,进行移除connectionManager对象

2. Spring单例模式

注意事项

        OSS中还有其他对象也需要手动进行close,否则还是会出现内存溢出的情况。如 OSSObject等,需要具体情况具体进行分析。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐