记录一次集合使用不当导致线上内存泄漏问题

目录

一、业务场景

二、事故发生

三、事故排查

1.排查

2.定位

四、总结 


一、业务场景

        本来只是一次普通的优化改动,运维会定期排查一些慢sql,而对应微服务的负责人需要去协调人员情况去处理各个域的慢sql问题。而这个sql就到了我的手上。

        改动点是一个站内信未读信息数量的查询sql优化改动,业务中站内信分为不同的消息类型,各自的消息类型又有不同的类型标签,每次查询用户不用消息类型的站内信未读数量时都需要从数据库中全量查一下该类型下的所有类型标签,而这些消息类型标签是基本长期不会改动的,而这个查询接口在app首页调用频率极高。所以优化考虑将这些类型标签放置于本地缓存中,每次查询时直接从缓存中取,这样能大大减少数据库查询次数,提升性能。

二、事故发生

        代码逻辑进行优化后,只进行了简单的自测当晚就发布上线了,第二天十点左右,该服务开始陆续进行预警,然后该服务就OOM挂掉了,紧接着其它服务业务陆续报异常,发现后紧急让运维回退了昨天发布前回滚。当时第一反应就是昨天上线的代码有内存泄露风险bug,所以回滚业务正常后就第一时间去翻了昨天优化的代码逻辑,当然我自己翻来覆去看都没有发现问题,接着让部门主管也帮着分析,但是大家在最开始都没有看出来问题在哪?可能是这个问题真的很小并且在日常中容易被遗漏忽略掉。

三、事故排查

1.排查

        紧接着开始排查dump文件

        使用JProfiler打开dump文件进行分析,可以看到最大的两个类,点进去进行详细分析,可以看到都是select语句。

         打开详细信息,select in 语句拼接了大量的id,以此定位了问题所在,找到这个sql使用的地方,发现就是昨天优化上线的查询代码,将消息类型id写在了本地缓存中,因为List集合的错误使用方式,导致消息类型id一直无限的重复向里边set,最终导致了程序的OOM。

2.定位

        让我们定位到程序代码中,这是最开始导致OOM的代码。

//本地缓存消息类型
private Map<Integer, List<MsgType>> msgTypeCacheMap = new ConcurrentHashMap<>();

//在程序启动加载时,就查询数据库将所有消息类型写入到本地缓存map中
@PostConstruct
    public void init() {
        log.info("===初始化消息类型开始===");
        List<MsgType> msgTypeList = lambdaQuery().list();
        if (CollUtil.isEmpty(msgTypeList)) {
            log.info("===消息类型为空,初始化消息类型结束===");
            return;
        }
        msgTypeCacheMap = msgTypeList.stream()
                .collect(Collectors.groupingBy(MsgType::getChannel));
        log.info("===初始化消息类型结束===");
    }

         就是在这里出现的内存泄漏,每次来查询都会调用一次,原先此接口是直读数据库,现在是直接写到了内存中,直接get出对应消息类型集合后,下意识地认为这就是一个新的集合,所以直接对此进行了操作,每次都会又向其中add了新的集合数据,但实际上,get出的集合地址引用的还是内存中的原地址,所以每次add改动,都是直接改动了内存中的这个集合,所以导致每次查询,每次add,无限增大,最终导致内存泄漏,服务宕机。


public List<MsgType> getMsgTypeListByCache(Integer channel) {
        if (MailConstants.WORKER_CHANNEL == channel) {
            return msgTypeCacheMap.get(channel);
        } else {
            List<MsgType> msgTypes = msgTypeCacheMap.get(MailConstants.PROVIDER_CHANNEL);
            //上边get之后,下边直接set进行返回,导致该list无限增大,最终OOM
            msgTypes.addAll(msgTypeCacheMap.get(MailConstants.WORKER_CHANNEL));
            return msgTypes;
        }
    }

        后续也是对此处的代码进行了处理,测试无误后重新发布上线。

四、总结 

        其实不管是在日常工作或者是在学习中,往往那些错误都是一些最常见的使用错误,或者说存心大意造成的,说句实话其实遇到最多的异常就是空指针异常,这个最常见也是最容易被忽略的简单问题,它可能出现在任何地方。而这次OOM事件也是给我自己上了一课。类似这种错误,如果不进行压测是测不出来的,如果只是进行简单的功能测试,是无法暴露出问题的。所以更需要开发人员在工作中,对于内存的使用要注意,不要因为只是一些简单的方法或者接口就去忽略的内存本身使用的风险。

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android线内存泄露是指在Android应用程序中存在未释放的内存对象,导致应用程序的内存使用量不断增加,最终可能导致应用程序崩溃或运行缓慢。 常见的Android线内存泄露原因包括: 1. 静态变量引用:在Android应用中,如果一个对象被一个静态变量引用,那么即使该对象不再需要,系统也无法释放它占用的内存。这种情况下,如果静态变量的生命周期比应用的生命周期长,就会导致内存泄露。 2. 匿名内部类和非静态内部类引用:在Android开发中经常使用匿名内部类或非静态内部类,如果这些类引用了外部类的实例,那么外部类的实例就无法被释放,从而导致内存泄露。 3. Handler和Thread引用:在Android中,使用Handler和Thread时需要注意是否正确释放相关资源,否则可能导致内存泄露。 4. 资源未关闭:在使用一些需要手动关闭的资源,如数据库连接、文件流、网络连接等时,如果没有及时关闭这些资源,就会导致内存泄露。 5. 单例模式的不当使用:如果一个类被设计为单例模式,并且该类持有了大量的数据或引用其他对象,那么该对象的生命周期将与应用程序的生命周期相同,容易导致内存泄露。 要解决Android线内存泄露问题,可以采取以下步骤: 1. 使用工具进行内存泄露分析:Android平台提供了一些工具,如Android Profiler和LeakCanary等,可以帮助定位内存泄漏的原因和位置。 2. 检查代码:仔细检查代码,确保正确地释放对象,包括关闭资源、取消注册等操作。 3. 避免静态引用:尽量避免使用静态引用,特别是对于大对象或持有其他对象引用的对象。 4. 使用弱引用:对于可能导致内存泄露的对象,可以考虑使用弱引用来引用它们,这样当没有强引用指向它们时,系统可以自动回收它们。 5. 注意生命周期:在设计和使用对象时,要注意对象的生命周期,尽量使其与应用程序的生命周期相一致,避免对象持有过长时间。 6. 及时释放资源:在使用需要手动关闭的资源时,要及时关闭这些资源,避免资源泄露。 综上所述,通过分析内存泄漏的原因,并采取相应的措施来修复和预防内存泄漏问题,可以提高Android应用程序的性能和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值