一次腾讯云 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 的内存快照:
2. Java 自带的工具:
jstat 命令:用于监控 JVM 的状态。例如,可以使用以下命令监控 JVM 的垃圾回收情况:
jstack 命令:用于生成 Java 线程的堆栈信息。例如,可以使用以下命令生成 Java 线程的堆栈信息:
三、修复内存泄露
一旦找到内存泄露的原因,就需要修复内存泄露。修复内存泄露的方法因情况而异。以下是一些常见的修复方法:
1. 关闭资源:如果应用程序使用了一些资源,例如数据库连接、文件句柄等,那么需要在使用完后及时关闭这些资源,以释放内存。
2. 优化代码:如果应用程序中存在一些不必要的对象创建和引用,那么可以通过优化代码来减少内存使用量。
3. 调整 JVM 参数:可以通过调整 JVM 参数来优化内存使用。例如,可以增加堆内存大小、调整垃圾回收策略等。
4. 使用内存泄露检测工具:可以使用一些内存泄露检测工具来帮助识别和修复内存泄露。例如,可以使用 Eclipse Memory Analyzer 等工具来检测内存泄露。
四、预防内存泄露
最后,需要预防内存泄露。以下是一些预防内存泄露的方法:
1. 及时释放资源:在使用完资源后,需要及时释放资源,以避免内存泄露。
2. 避免创建不必要的对象:在编写代码时,需要避免创建不必要的对象,以减少内存使用量。
3. 使用缓存:可以使用缓存来避免重复创建对象,以减少内存使用量。
4. 定期检查内存使用情况:定期检查内存使用情况,可以及时发现内存泄露问题,并采取相应的措施。
总之,解决线上 JVM 内存泄露需要识别内存泄露、分析内存泄露原因、修复内存泄露和预防内存泄露。通过以上方法,可以有效地解决线上 JVM 内存泄露问题。
版权声明: 本文为 InfoQ 作者【波】的原创文章。
原文链接:【http://xie.infoq.cn/article/b198ead1a7852f0351b6e73a5】。文章转载请联系作者。
评论