Skip to content

记一个线上内存泄露排查的case #5

@gaoxianglong

Description

@gaoxianglong

现象

服务是I/O密集型场景,基于ParNewGC+CMS的GC组合,吞吐量优先。现象呢,就是S0/S1空间交换后的存活对象晋升到老年代后,触发了CMS的回收阈值(>=65%),但存活对象一直无法回收,导致持续Full GC,程序长时间STW,程序几乎无法对外提供服务。
问题排查的难点在于:

  • 老年代设置过小(500m,≈300m触发Full GC),常驻内存对象和内存泄露对象耦合在一起,不太好区分;
  • 没有GCRoot深堆占比很大的对象;

问题定位

线上故障时保留了几台现场,但dump下来的文件不太好直接定位出问题,因此临时方案是先把老年代扩大到1g,然后持续观察一段时间后,再多dump几份文件下来一起分析。通过对比后发现,故障现场的对象数量多出了近2倍+,这是一个非常大的疑点。然后切到柱状图,发现对象数量占比最高的就是AtomicLong、String、ConcurrentHashMap等这几个类型,如图1所示。
image

图1 故障现场的柱状图

很显然,故障现场存在大量的强引用,如果这些对象持续可达,那么GC必然无法回收,最终肯定会演变成内存泄露的聚集点。思考下,正常情况下,程序中怎么可能会创建这么多的AtomicLong对象?其次**这些对象肯定是被关联在成员变量的集合类型中才导致作用域无法结束而释放不掉**。为了验证这个结论,需要继续结合支配图来解剖对象的支配关系,如图2所示。 image
图2 故障现场的支配图

丛支配图来看,这些对象的间接或直接支配者都是Yammer Metrics,这是一个监控上报组件。其中String类型的深堆占比非常大(由Yammer Metrics缓存了服务的注册信息、RPC上下文信息等),因此排除掉弱引用、虚引用、软引用后,发现GCRoot路径都是发生在RPC请求时,如图3所示。 image
图3 故障现场的Path To GCRoots

因此得出的结论就是,程序中使用Yammer Metrics上报的地方没有及时清理缓存项才导致的线上服务内存泄露。

写在最后

这里有个插曲,整个BG的GC配置都一样,为啥偏偏就只有某个服务出现了这个问题。这是因为目标服务的发版频率较低,同时老年代设置过小,所以才爆出了这个问题。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions