【程序大侠传】应用内存缓步攀升,告警如影随形
前序
在武侠编码的江湖中,内存泄漏犹如隐秘杀手,潜伏于应用程序的各个角落,悄无声息地吞噬着系统资源。若不及时发现和解决,必将导致内存枯竭,应用崩溃。
背景:内存泄漏的由来内存泄漏,乃程序运行过程中,已不再使用的内存块未被及时回收,导致内存使用量不断增加的现象。此问题多发于对象生命周期管理不当之处,如持有对象引用过长,或未能及时释放资源,终致内存枯竭,系统崩溃。
在 JVM 的世界中,内存泄漏常见于以下几种情况:
静态集合类:如 HashMap、ArrayList 等,若不断向其添加对象而不清理,易造成内存泄漏。
长生命周期对象持有短生命周期对象引用:如单例模式中的对象持有临时对象引用,导致临时对象无法被垃圾回收。
未关闭的资源:如数据库连接、文件流等,若未及时关闭,亦会导致内存泄漏。
监听器与回调函数:未及时移除的监听器或回调函数,可能导致对象无法被回收。
解决方案:内存泄漏的破解之道
善用工具,探查隐患如同侠客需借助兵器,程序员亦需运用内存分析工具,如 jvisualvm、jmap、jhat 等,探查内存使用情况,定位内存泄漏之源。常规步骤如下:
使用 jmap 生成堆转储文件:jmap -dump:live,format=b,file=heap_dump.hprof <pid>
使用 jvisualvm 或 Eclipse MAT 分析堆转储文件,查找无法回收的对象。
优化代码,清理内存针对发现的内存泄漏问题,需优化代码,确保对象在不再使用时尽快释放。具体方法如下:
及时清理集合:对于使用完毕的对象,及时从集合中移除。
合理管理对象引用:避免长生命周期对象持有短生命周期对象引用,可使用弱引用(WeakReference)来管理。
关闭资源:对于文件流、数据库连接等资源,在使用完毕后,务必调用 close() 方法关闭。
移除监听器:在适当时机,移除不再需要的监听器或回调函数。加强监控,防患未然
如同江湖侠客需时刻警惕,程序员亦需持续监控内存使用情况,防患于未然。可使用监控工具如 Prometheus、Grafana 等,实时查看内存使用情况,及时发现异常。
滴滴滴、滴滴滴....,代码剑宗中的某一处洞府中的告警声不绝于耳,近眼望去,洞府正中央的蒲团上正坐着一位双眼紧闭身材健硕的男子,此男子的旁边还摆放着篮球、杠铃等道具,从洞府的摆布不难看出此男子平日经常锻炼,以至于他的身型较于常人更加挺拔高大。男子听到告警声睁开双眼,男子双眸中充满精光,看来此次闭关男子有了不少收获。听到警告声的男子眼神中闪过了一丝不耐烦,嘴里轻轻碎了一声,然后不紧不慢地从袖中拿出一个圆盘,此圆盘此时一直闪烁着红光,并且一直发出“滴滴滴”的声音,男子用手轻抚手中圆盘,身前映射出一个巨大光影,光影里面有一些画面跟文字,男子大概花了一刻钟时间扫描完光影里的内容。他脸上闪过一丝苦涩,然后说道:“该死的,竟然出现了内存告警”,随即只见男子双手一挥把光影打散,男子站了起来朝着旁边的一个房间走去。
而洞府中的男子就是咱们的男主“阿强”。他之前正坐在蒲团上修炼内功到一个关键时刻,不曾想被圆盘的告警打断,此时的阿强心情不是很美丽.......
内存紧急处理
阿强离开洞府的第一件事情就是通过告警身份牌进入“乾坤内存法阵”查看告警的应用阵脚,阿强查看应用的内存情况跟系统的一些指标如下图所示,阿强看到这个内存水位情况就发现了不对劲。应用从晚上 03:00 开始到目前为止内存一直处于一个缓慢上升状态。不过此时阿强倒也没有因此慌了自己阵脚,阿强根据以前处理类似问题的经验,他先通过“乾坤内存法阵”中的应用内存 Dump 导出功能先将内存快照给 dump 下来,然后就将应用的容器进行重启的操作。
实时区间热点图
实时线程数
实时 gc 数量
实时堆内存信息
容器实时内存情况
不久,应用的快照文件就 dump 了下来,阿强看着 dump 下来的文件并没有直接去分析而是优先去询问了负责此应用的人询问了一下具体情况。2 个时辰后,阿强大概从负责此应用的人口中知道了此应用的基本情况。此应用名叫 G 服务,是从 F 服务中拆出来的一个应用,拆出来的 G 服务的代码内容与 G 服务是保持一致的,但是 G 服务的内存表现很稳定,并没有 F 服务表现出来的内存缓慢爬升的情况,而 G 服务表现内存缓慢爬升则是随着不断提高流量灰度的一同上升的。其中 F 服务的一个容器内存情况大致是 8 台内存 16G 的云服务器,G 服务的容器内存情况是 4 台 8G 的云服务器。还有一个值得注意的一个点则是 G 服务的调用链路由于处于流量切换的过程与在 F 服务中不同,其区别如:
其中橙色的线表示 G 服务从 F 服务拆分出来后多一次交互,也就是说,在流量切换灰度期间,G 服务的流量入口是从 F 服务通过 rpc 接口方式接受的。
此时的阿强大概了解了 G 服务应用的基本情况,接下来要做的事情则是去分析内存缓慢爬升的问题,只见他拿出了一法器,此法器名叫“乾坤内存镜”,此法器的作用就是能够清晰地分析应用内存快照文文件,在使用法器有一个细节问题需要注意的是,如果通过此法器直接去打开内存快照文件,此法器会默认进行 fullgc,fullgc 后的快照文件如下图:
fullgc 后的快照文件内存大小明显和实际占用不同,如果想让法器打开快照文件不尽兴 fullgc,则只需要换一种打开方式,打开方式如下:
此时你应该能看到如下图的内容说明此次打开方式没有尽兴 fullgc,我们只需要稍等片刻即可
通过这种方式打开的快照文件则是如下所示:
阿强看着解析出来的快照内容,此时展现出来内容是通过内存的实例的数量来进行的排序,其中,char[]占用了 1412m 大小的内存,粗略看下来没有什么大对象。如果是几年前的阿强,他会傻不拉几地去查看 char[]实例的引用,但是此时的阿强已经不是两年前的阿强,经过时长两年半的练习,他踩过数不清的坑,经验告诉他,此时你应该看看第三个实例,阿强此时查看第三个实例的 Merged outgoing references,他看到此实例的引用
然后再进一步跟进 String 的引用,除了 spring 的常规引用,发现 logback 和 jackson 有引用大量的字符实例。
阿强此时通过 idea 打开了 G 服务的代码,开始查看起来 jackson 和 logback 的代码使用点,2 个时辰后,阿强发现了一些奇怪的日志打印,如下:
上面这种日志打印会将整个请求的入参都打印出来,而且一次请求类似这种打印全部请求入参的日日志大概有 5~7 次,而由于 G 服务的承载的业务请求报文都是比较大的,也就是说,每一次请求过来,这种大日子的打印会打印好几次,而这些大而全的日志大部分内容是没用的,并且每次打印生成的字符串每次都是不同的,也就是每次请求在堆内存中生成 6~7 个大字符对象,这种大字符对象会在堆中频繁创建,会造成 youngc 很频繁。而 youngc 过于频繁会造成很多大字符对象进入老年代,导致整个堆内存不断上升。为了验证自己的猜想,阿强尝试着删除 G 服务中这些大日志的打印,最终发现内存上升的情况有一定的改善(此时的内存已经不会出现缓慢爬坡的情况),但是内存表现相比较 F 服务还是没有那么好的,因此阿强又只能进一步去分析内存块照文件,2 刻钟后,阿强在线程 ThreadLocal 中发现很多大 char[]数组的引用,而这些 ThreadLocal 都是由 rpc 线程所持有。
而 rpc 底层的序列化正是使用的 jackson,而 com.fasterxml.jackson.core.util.BufferRecycler 是 Jackson 库中的一个工具类,用于高效地管理和重用缓冲区。在多线程环境中,特别是使用 ThreadLocal 时,确实有可能导致内存泄漏:
ThreadLocal 的生命周期问题:ThreadLocal 变量会与线程的生命周期绑定,如果线程不被回收,ThreadLocal 变量及其引用的对象也不会被回收,可能导致内存泄漏。
缓冲区的大小和数量:如果缓冲区的大小或数量非常大,且这些缓冲区长期占用内存而不被释放,可能导致内存使用过多,从而引发内存泄漏。
线程池使用不当:在使用线程池时,如果没有正确管理线程池的生命周期和资源,可能会导致线程无法被回收,进而导致 ThreadLocal 引用的对象无法被回收。
到这里真相大白,而阿强面对涉及基础设施的改造,他有点烦躁。凡是涉及基础设施的改动,任务的难度和解决时间就会成倍增加,因为基础设施的改造流程会拉的比较长。但这个任务是一个紧急的任务,为了快速地将问题处理,那怎么能够不去改造基础设施并解决这个问题呢,阿强脑子在飞速的运转,不多时,阿强心中闪过一丝光亮,他紧皱的眉间也开始舒坦。刚刚的那一丝光亮就是快速解决任务的关键,那就是:“类加载器的双亲委派机制!!”
版权声明: 本文为 InfoQ 作者【Disaster】的原创文章。
原文链接:【http://xie.infoq.cn/article/47d93f76096ec04c19c384eb1】。文章转载请联系作者。
评论