线上环境出现内存泄漏怎么在不重启的情况下定位
摘要:# 线上内存泄漏,别急着重启!这几个“土办法”比想象中管用 你肯定遇到过这种情况:线上服务跑得好好的,突然响应越来越慢,监控面板上那条内存占用曲线,像个不听话的孩子,一路向上爬,眼看就要冲破警戒线了。重启?简单粗暴,业务立马恢复,但问题就像打地鼠,过一阵…
线上内存泄漏,别急着重启!这几个“土办法”比想象中管用
你肯定遇到过这种情况:线上服务跑得好好的,突然响应越来越慢,监控面板上那条内存占用曲线,像个不听话的孩子,一路向上爬,眼看就要冲破警戒线了。重启?简单粗暴,业务立马恢复,但问题就像打地鼠,过一阵子又冒出来,而且你根本不知道洞里到底藏着什么。
说实话,很多团队的第一反应就是“先重启保业务”,这没错。但重启就像给发烧的病人吃退烧药,烧是退了,病灶却没找到。今天,咱们就聊聊,在不惊动业务、不重启服务的前提下,怎么把那个偷偷“吃掉”内存的元凶给揪出来。
先稳住,别慌!内存涨不一定就是泄漏
内存使用量持续上涨,不一定就是代码写漏了。你得先排除几个“假警报”:
- 是不是业务量真上来了? 新功能上线、促销活动,真实请求量暴增,内存跟着涨是天经地义。看看QPS(每秒查询率)曲线是不是和内存曲线肩并肩一起往上冲。
- 是不是缓存策略没设好? 本地缓存没设过期时间或者LRU(最近最少使用)策略失效,缓存无限膨胀,吃光内存太正常了。检查一下你的Guava Cache、Caffeine或者Ehcache配置。
- JVM堆内存分配是不是太小了? 如果堆内存设得抠抠搜搜,而业务正常,那GC(垃圾回收)会非常频繁,虽然看起来内存使用率不高,但频繁GC本身就会导致性能下降,让你误以为是内存不够。这时候,适当调大堆内存可能就解决了。
如果排除了这些,内存占用依然只增不减,像个无底洞,那大概率就是经典的内存泄漏了。 说白了,就是有些对象,你已经不用了,但代码里还有地方“拽着”它不让垃圾回收器收走。时间一长,这种“垃圾”越堆越多。
不重启,我们手里有什么“武器”?
好了,假设你确定就是泄漏,而且业务还在跑,不能重启。这时候,你需要的是对正在运行的JVM进行“在线诊断”。别怕,这听起来高大上,其实用对工具,就像用听诊器一样自然。
第一招:用JDK自带的“老中医”号个脉
JDK自己就带了一套强大的诊断工具,就在你安装目录的bin下面。别看不起它们,老中医有时候比新式仪器还准。
jps:先看看你家服务在系统里的“身份证号”(PID)。命令行敲个jps -l,一眼就能找到。jstat:这是看“新陈代谢”的。最常用的是jstat -gcutil <pid> 1000。这个命令会每隔1秒(1000毫秒)打印一次堆内存各区域(Eden区、Survivor区、老年代等)的使用百分比和GC次数/时间。如果老年代(O)的使用率一直稳步上涨,并且Full GC后也降不下来多少,那基本就是泄漏的铁证了。jmap:这是“拍X光片”的。用它生成堆内存快照(Heap Dump)是定位问题的黄金标准。- 命令:
jmap -dump:live,format=b,file=heap.hprof <pid> - 注意:
-dump:live会触发一次Full GC,只dump存活的对象。这对线上服务有一定影响,可能会引起短暂的停顿(STW)。所以,最好在业务低峰期操作,或者先和业务方打个招呼。但比起直接重启,这个影响通常小得多,且能拿到关键证据。
- 命令:
第二招:分析“X光片”,找到病灶
拿到了heap.hprof这个快照文件,接下来就是分析。我个人的习惯是直接用 Eclipse MAT 或者 JProfiler。这里以MAT为例,因为它免费且强大。
- 打开快照:用MAT加载你的
.hprof文件。 - 看“概览”:MAT首页就会给你几个醒目的嫌疑犯提示,比如“Leak Suspects Report”(泄漏嫌疑报告)。它经常能直接告诉你,有一个大对象,被谁谁谁引用着,占了百分之多少的内存。很多时候,问题就这么直接暴露了。
- 深入分析:如果概览不够清晰,就用“Dominator Tree”(支配树)视图。这里按对象 retained size(即这个对象本身加上它引用的所有对象的总大小)排序。排在最前面的,往往就是内存消耗大户。点进去,看它的“Path To GC Roots”(到GC根的路径),排除弱引用、软引用等,只看强引用链。这条链,就是阻止它被回收的“罪证链”。顺着这条链,你就能找到代码里是哪个静态变量、缓存或者线程局部变量一直抓着这些对象不放。
第三招:Arthas——线上诊断的“瑞士军刀”
如果觉得上面步骤还是有点重,或者你想更动态、更实时地观察,那Arthas绝对是你的菜。阿里开源的这个工具,堪称Java工程师的线上救火神器。它可以直接attach到运行中的JVM,无需修改代码,无需重启。
dashboard:一个命令,整体情况全掌握,实时看到内存、线程、GC信息。heapdump:没错,它也能dump堆内存,和jmap效果一样,但集成在同一个工具里更方便。monitor/watch/trace:这是定位泄漏源的“大杀器”。你可以通过监控某个可疑方法的调用次数、参数、返回值,或者追踪方法的内部调用链路。比如,你怀疑是某个查询方法每次都创建大对象且没释放,就可以用watch com.example.Service queryMethod "{params, returnObj}"来观察。看到返回的对象数量异常堆积,基本就锁定了。
举个例子:我之前遇到一个案例,一个后台任务每隔几分钟就产生一批几十MB的报表对象,本应用完就丢,结果被一个全局的List不小心add了进去。用Arthas的 watch命令监控那个add方法,很快就看到那个List在疯狂增长,问题一目了然。
几个常见的“泄漏坑”和“止血思路”
定位到具体代码后,你会发现,内存泄漏的花样其实就那几种:
- 静态集合类滥用:比如
static Map或static List不断往里放数据,只放不删。这是最常见、最low但也最容易犯的错。 - 连接未关闭:数据库连接、网络连接、文件流,用了没关。务必用try-with-resources语法!
- 监听器未注销:注册了各种事件监听器,对象销毁时忘了反注册,导致自己被框架长期引用。
- ThreadLocal使用不当:线程池环境下,线程复用,上一个任务塞进ThreadLocal的值没清理,下一个任务来了还能读到,而且一直积累。用完一定要
threadLocal.remove()。 - 缓存当成仓库:还是那句话,没有大小限制和过期策略的缓存,就是内存泄漏的合法外衣。
临时止血:如果情况紧急,定位需要时间,可以尝试一些临时措施:
- 如果是缓存问题,且缓存支持动态配置,立刻调低缓存大小或缩短过期时间。
- 如果是某些特定API或任务导致,可以考虑在负载均衡层面先将其暂时下线或降级。
- 谨慎使用
jmap -histo:live <pid>:这个命令会触发Full GC并统计直方图,能快速看到哪些类的对象最多,有时能提供线索,但同样有STW影响。
最后说点大实话
内存泄漏的定位,三分靠工具,七分靠经验和对业务代码的熟悉。工具告诉你“谁占了大房子”,但“为什么他占着不走”,还得你自己去代码里破案。
养成好习惯比什么都强:代码Review时多看一眼集合的使用;上线前用-XX:+HeapDumpOnOutOfMemoryError参数,让JVM在OOM时自动留下去世前的“遗言”(堆快照);定期在预发环境做压力测试,观察内存曲线。
线上出了问题,不重启就搞定,这种成就感可比单纯点一下重启按钮爽多了。毕竟,咱们是工程师,不是重启师,对吧?
行了,工具和方法都摆在这儿了,下次内存再报警,知道该怎么下手了吧?稳住,你能赢。

