大家好,我是小米,一个总是乐于分享技术的小伙伴!今天,我们来聊一个在实际开发过程中非常常见、但又非常棘手的异常问题——OutOfMemoryError。你是不是也曾经遇到过类似的情况:明明按道理内存已经够用了,为什么程序还是会报OutOfMemoryError呢?今天我就通过一个真实的案例和大家分享一下,我是如何一步步排查和优化,最终解决这个问题的。
问题背景这次问题的起因是我们电商系统首页的视频上传功能。作为后台的技术负责人,我接到运营同学的反馈,运营在后台上传一个大约30M的视频时,居然报了操作失败的错误。系统提示java.lang.OutOfMemoryError。看到这里,我的第一反应就是,内存应该是没问题的啊!因为几天前,我刚刚把堆内存从512M调整到2G,当时是因为一个2M的图片上传也报了同样的错误。
怎么会发生这种情况呢?这不禁让我产生了疑问,难道内存问题又回来了?于是,我决定亲自去排查一番。
排查日志首先,我打开日志文件,果然,看到了java.lang.OutOfMemoryError的报错信息。错误发生的时间点很清晰,就是在运营上传视频时。我对这个报错做了个初步的判断:视频文件毕竟比图片大,内存占用量也会更高。可是,堆内存已经调到2G了,照理说,不应该再报内存溢出才对。
为了更深入地了解问题,我决定进一步排查。于是,我用jps命令查找了相关进程的PID。通过命令:
我得到了当前运行中的Java进程ID。然后,我通过以下命令获取堆内存使用情况:
查看结果后,我发现堆内存的使用率已经达到了100%。哇,这个数据让我一下子愣住了!明明堆内存配置成2G了,怎么会直接满呢?(PS:当时的图忘记截了,用下面这个图占位,大家可以尽情想象一下)
接下来的操作就是导出堆Dump文件,以便深入分析问题。在jhsdb工具中,我运行了以下命令,将堆信息导出为.hprof文件:
这个命令成功导出了1.8G的堆Dump文件,准备进入下一步分析。堆Dump文件的大小让我更加确信,这个问题一定与内存的占用有关,且可能存在大量的对象在堆中堆积。
使用VisualVM分析堆Dump文件堆Dump文件导出后,我使用了VisualVM工具来对其进行分析。VisualVM是一个强大的Java性能分析工具,能够帮助我们查看堆内存的使用情况、对象的分布以及各个类的内存占用。
通过VisualVM,我发现堆中确实存在一些异常大的对象,其中有15个对象的大小都超过了15M,最大的大对象甚至达到了70M!这些对象占用了大量的内存,而且堆内存的分配也非常不均匀。为了进一步排查,我仔细查看了这些大对象的内容,发现它们都是视频的byte[]数组。
这下我明白了,问题的关键就出在这些视频数据上。每上传一个视频,视频内容会以byte[]数组的形式存储在内存中。而且,这个数组的大小竟然达到70M,而且通过不停的复制创建新对象,堆内存瞬间就被这些大对象填满,导致了OutOfMemoryError的异常。
你以为是70M,但实际上是70M的N次方……
分析原因通过以上排查,我大致找到了问题的根源。这些byte[]数组是视频上传的主要负载,但它们并不是必须返回给前端的。前端需要的只是视频的OSS地址,而不需要实际的视频数据。因此,返回给前端的这些视频内容,实际上是完全可以去除的。
除了这个原因外,我还发现了一些额外的问题。我们的系统中有多个拦截器,这些拦截器会对每一个接口的请求进行拦截处理。在拦截器中,视频文件的数据会被一次又一次地复制和生成对象,这就导致了堆内存中大量无效对象的生成,进一步加剧了内存的消耗。
另外,我发现视频内容的byte[]数组在初始化ElasticSearch(ES)时也被错误地传递了。通常来说,ES并不会使用到视频的内容,而只是存储一些元数据,因此传递视频数据到ES并没有意义。这个问题的存在也是导致内存占用过高的一个原因。
优化措施根据排查结果,我做出了以下几项优化措施:
1. 移除视频内容的返回
视频上传接口中,之前会将视频的内容(byte[]数组)返回给前端。这个做法是不必要的,因为前端只需要视频的OSS地址即可,而不是视频的实际内容。于是,我修改了接口,去掉了返回byte[]数组的部分,只返回视频的OSS地址。这样,前端无需再接收大文件,减少了内存占用。
2. 优化拦截器
系统中有多个拦截器,在处理视频上传时,会根据视频的内容生成多个对象,这导致了内存占用的急剧增加。为了优化这一点,我对拦截器做了针对性调整。具体来说,我在拦截器中增加了条件判断,只对必要的请求生成对象,并过滤掉上传文件相关的无效对象。这样,内存中的无效对象就大大减少了。
3. 移除ES中的视频数据
在初始化ES时,错误地将视频内容的byte[]数组也传递给了ES。这个操作并没有任何实际意义,因为ES一般只会使用视频的元数据。因此,我在代码中移除了这部分不必要的内容,避免了无用的内存消耗。
结论经过上述优化后,系统的内存占用得到了有效控制,视频上传功能也恢复了正常,不再报OutOfMemoryError了。通过排查,我不仅发现了内存使用过多的原因,还通过一系列优化,减少了无效的内存占用,提高了系统的性能和稳定性。
从这个案例中,我们也可以总结出几点经验教训:
细致排查:面对OutOfMemoryError异常时,一定要细致地排查内存中的大对象,找到根本原因。
合理优化:不仅要关注功能的实现,更要关注内存的使用效率,避免无效的对象和数据占用过多内存。
工具的使用:VisualVM和堆Dump文件是非常有效的内存分析工具,可以帮助我们快速找到内存泄漏和占用过多的对象。
END希望这篇文章能帮助大家更好地理解和解决类似的内存问题。如果你也遇到过类似的挑战,欢迎留言讨论!我们一起进步,一起解决问题!
感谢阅读,我们下期再见!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!