WeakReference 使用不当导致OOM?

9月5、6号两天,有个古老的应用连续出现频繁的 FullGC,最终OOM。
这个古老应用的框架是十多年前基于 Spring 魔改的,在部署上分为 web、app 两层,web通过 EJB 进行调用app里的服务访问数据库。

这两层访问 Spring 容器里的 Bean 一般都是通过类似下面的写法来获取:

SomeService service = CustAppContextHolder.getContext(contextName).getBean(beanName);
service.doSomething();

// CustAppContextHolder.getContext 的实现大致如下:
public class CustAppContextHolder {
private static Map<String, WeakReference> caches = new ConcurrentHashMap<String, WeakReference>();

public static ApplicationContext getContext(String contextName) {
    WeakReference<ApplicationContext> weakReference = caches.get(contextName);
    ApplicationContext context;
    if (weakReference != null && (context = weakReference.get()) != null) {
        return context;
    } else {
        synchronized (CustAppContextHolder.class) {
            context = initContext(contextName);
            caches.put(contextName, new WeakReference<>(context));
        }
    }
    return context;
}

private static ApplicationContext initContext(String contextName) {
    System.out.println("init context:" + contextName);
    return null;
}

}

从内存 dump 分析,ZookeeperRegistry.subscribed 对象占用了内存导致没法回收,同一个服务,里面好几千个消费者实例。每个服务对应的 URL 里带有一个时间戳,导致进行服务订阅时都认为是不同的服务。
首先找负责该 RPC 框架的团队,他说通过这种方式 SomeService service = CustAppContextHolder.getContext(contextName).getBean(beanName); 获取 RPC 消费者时,每次都会创建一个新的实例,这个实例会在 ZK 注册。这种写法其实一直是这样的,十多年了,中间经历了从 weblogic 切换到信创基于 apusic 的中间件。

第一天 5号排查时主要围绕这个 RPC 调用的地方进行,但从代码看,很多服务都没有进行调用了,但仍然有消费者实例。因此说是 RPC 调用导致是说不通的。

晚上回来又想了想,不是 RPC 被调用了,而应该是容器被初始化,导致创建了 RPC 消费者实例。
第二天到了公司,查了下日志,5号那天,有接近1万次的 “init context:” 输出,也就是说应用容器被创建了近上万次,怪不得那么多消费者实例。在4号发版之前的日志里,每天也有小几百次的初始化日志输出,不至于把内存占用太多。
输出日志的线程都是 ORBWorker,后来咨询负责 apusic 中间件的团队,说这个是 apusic 负责处理 EJB 调用的线程。他们还指出可能是 CustAppContextHolder 使用 WeakReference 导致上下文被回收后,再次来获取上下文时创建新的,这个老框架的下一个大版本就把这个 WeakReference 移除了。

这个问题目前还没稳定地复现出来,生产上也是个别应用实例会 OOM,如果真是这个 WeakReference 导致的,可能的优化方案有:
1、在应用代码里定义一个跟 CustAppContextHolder 包和类名完全一样的类,把 WeakReference 移除掉,通过类加载优先级替换框架的类。
2、针对 WeakReference 的特定,它引用的对象在没有强引用时可以被 GC 回收,定义静态变量来维持强引用,保证初始化的上下文不会被回收,从而可以复用,减少 RPC 消费者的实例。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据