一次腾讯云COS SDK线上内存泄漏问题总结

JVM内存泄露是Java应用程序中常见的问题之一。当应用程序在运行时,如果没有正确地释放内存,就会导致内存泄露。这会导致应用程序的性能下降,甚至会导致应用程序崩溃。本文将分享一次对腾讯云COS SDK线上内存泄漏问题排查的过程。并对Java泄漏问题的处理方法进行一些总结,期望能帮助到正在被Java内存泄漏困扰着的同学。

业务系统配置

1)JDK的版本信息

openjdk version "1.8.0_232"

OpenJDK Runtime Environment (Tencent Kona 8.0.0-internal) (build 1.8.0_232-86)

OpenJDK 64-Bit Server VM (Tencent Kona 8.0.0-internal) (build 25.232-b86, mixed mode, sharing)

2)JVM的配置信息

-server -Xms4g -Xmx4g 

-XX:ActiveProcessorCount=4 

-XX:+UseAdaptiveSizePolicy 

  项目的业务场景是一个后台的定时任务,每天凌晨1点调用腾讯COS SDK拉取云COS相关的备份信息,并记录数据库,以便运营进行业务分析。考虑后台任务不用关注延迟,因为用了默认Parallel Scavenge(新生代)+Parallel Old(老年代),并开启UseAdaptiveSizePolicy自动调节新生代大小比例。

问题状况和排查过程

   项目上线运行一段时间以后,运营反馈数据更新有延迟。上线排查,发现日志存在大量的java.lang.OutOfMemoryError:GC overhead limit exceeded日志信息。怀疑是跟内存泄漏有关。因此重启服务 ,启动参数加上-XX:+HeapDumpOnOutOfMemoryError 和-XX:HeapDumpPath参数,这样就可以让JVM发生内存泄漏时候自动触发dump。

  通过MAT分析,发现存在大量的PoolingHttpClientConnectionManager,占了近2.5个G。

   通过OQL查询 一共有9366个对象实例,平均每个对象实例的Retained Heap占有295928个字节,换算一下9366 * 295928 =  2771661648 = 2.58131G,因此也符合上图所展示的数据。

   查看引用情况,可以看到有9364个对象实例是被com.qcloud.cos.http.IdleConnectionMonitorThread引用

   通过进一步分析,可以看到com.qcloud.cos.http.IdleConnectionMonitorThread是由com.qcloud.cos.http.DefaultCosHttpClient创建,CosHttpClient又由COSClient创建。

  因此这个链路就比较清晰,即是:

PoolingHttpClientConnectionManager -》IdleConnectionMonitorThread -》DefaultCosHttpClient -》COSClient

  而COSClient正是用于获取腾讯云COS备份信息,因此问题也跟业务背景相关。

  于是查源代码,可以看到每次创建一个DefaultCosHttpClient都会生成PoolingHttpClientConnectionManager和IdleConnectionMonitorThread的实例对象,并且将connectionManager作为参数传人到idleConnectionMonitor的构造函数中。

  查看IdleConnectionMonitorThread的具体实现,可以看到继承了Thread,是一个线程类。在调用上图中调用strart()方法,将会开启一个异步线程任务。

  IdleConnectionMonitorThread主要包含run和shutdown两个方法。

  在run方法中,循环判断shutdown变量为false时,等待2000MS,然后清除connMgr的无效链接。

  可以看到shutdown变量,是一个被volatile修饰的boolean值,这样就保证线程间的可见性。默认shutdown变量为false,一旦调用shutdown方法,shutdown变量将被设置为true,调用notifyAll()进行线程唤醒。此时run方法将立即执行剩余方法,并在下次循环中判断shutdown为true,跳出当前循环。

  此时当前idleConnectionMonitor的线程实例,由于线程任务已经执行完,将会被关闭。

  在下次GC回收时候,idleConnectionMonitor作为无效的GC Root会被回收。那么它所引用的connMgr,也就是前面的PoolingHttpClientConnectionManager的对象实例,通过可达性分析,将会被标记为不可达,也会一起被GC回收。

  通过如上所推论,需要在使用COSClient以后,通过shutdown方法,关闭IdleConnectionMonitorThread这个监控线程,从而回收PoolingHttpClientConnectionManager。

  查看业务代码,确实没有加上shutdown方法。因此随着时间积累,将产生大量无法回收的PoolingHttpClientConnectionManager的对象实例,从而最终导致内存泄漏。

  因此修改业务代码,加上shutdown方法以后,目前系统稳定运行,内存也恢复正常。

问题和总结

  在查阅腾讯云官网时候,确实发现有相关的提示,但提示并非很明显。在实际线上场景,会存在遗漏相关代码,造成内存泄漏现象。

  因此对于JVM的内存泄漏问题,除了在平时写代码时候,需要认真仔细以外。在发生线上故障时候,能通过经验和工具进行问题排查,也是很重要一部份。

  关于如何处理线上JVM内存泄露问题,可以从以下几方面考虑:

一、识别内存泄露

  首先,需要识别内存泄露。可以通过JVM的内存监控工具来检测内存泄露。例如,可以使用JConsole或VisualVM等工具来监控JVM的内存使用情况。如果发现内存使用量不断增加,而且没有明显的回收迹象,那么就有可能存在内存泄露。

二、分析内存泄露原因

1. Heap Dump快照:Heap Dump是JVM在应用程序运行时生成的内存快照。可以使用MAT(Memory Analyzer Tool)等工具来分析Heap Dump。通过分析Heap Dump,可以找到内存泄露的对象和引用链。这有助于找到内存泄露的原因。

Heap Dump快照有两种方式:

1)在启动参数加上-XX:+HeapDumpOnOutOfMemoryError 和-XX:HeapDumpPath参数

2)jmap命令:用于生成JVM的内存快照。例如,可以使用以下命令生成JVM的内存快照:

代码语言:javascript
复制
jmap -dump:format=b,file=heap.bin 

2. Java自带的工具:

jstat命令:用于监控JVM的状态。例如,可以使用以下命令监控JVM的垃圾回收情况:

代码语言:javascript
复制
jstat -gc   

     jstack命令:用于生成Java线程的堆栈信息。例如,可以使用以下命令生成Java线程的堆栈信息:

代码语言:javascript
复制
jstack <pid>

三、修复内存泄露

  一旦找到内存泄露的原因,就需要修复内存泄露。修复内存泄露的方法因情况而异。以下是一些常见的修复方法:

1. 关闭资源:如果应用程序使用了一些资源,例如数据库连接、文件句柄等,那么需要在使用完后及时关闭这些资源,以释放内存。

2. 优化代码:如果应用程序中存在一些不必要的对象创建和引用,那么可以通过优化代码来减少内存使用量。

3. 调整JVM参数:可以通过调整JVM参数来优化内存使用。例如,可以增加堆内存大小、调整垃圾回收策略等。

4. 使用内存泄露检测工具:可以使用一些内存泄露检测工具来帮助识别和修复内存泄露。例如,可以使用Eclipse Memory Analyzer等工具来检测内存泄露。

四、预防内存泄露

  最后,需要预防内存泄露。以下是一些预防内存泄露的方法:

1. 及时释放资源:在使用完资源后,需要及时释放资源,以避免内存泄露。

2. 避免创建不必要的对象:在编写代码时,需要避免创建不必要的对象,以减少内存使用量。

3. 使用缓存:可以使用缓存来避免重复创建对象,以减少内存使用量。

4. 定期检查内存使用情况:定期检查内存使用情况,可以及时发现内存泄露问题,并采取相应的措施。

  总之,解决线上JVM内存泄露需要识别内存泄露、分析内存泄露原因、修复内存泄露和预防内存泄露。通过以上方法,可以有效地解决线上JVM内存泄露问题。