• 记一次Java内存泄漏最终导致内存溢出的事故分析


    参考

    Java内存监控工具

    jps

    //获取 java进程的 pid
    jps -l
    
    • 1
    • 2

    jstat

    虚拟机统计信息监视工具

    //每2秒 获取一次 gc 信息
    jstat -gcutil pid 2000
    
    • 1
    • 2

    jmap

    //查看存活对象情况 导出日志信息
    //查看内存信息 实例个数 以及 占用内存大小
    jmap -histo:live pid > ./livelog.txt
    
    • 1
    • 2
    • 3
    //查看年轻代, 老年代 内存配置 和 使用情况
    //查看内存堆栈信息
    //8.0以前
    jmap -heap pid > ./log.txt
    
    //8.0以后
    jhsdb jmap --heap --pid pid > ./heaplog.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    jstack

    //打印堆栈信息
    jstack -l pid > ./stacklog.txt
    
    • 1
    • 2

    事故处理经过

    最近用户量激增 后台说我这个推送服务 占用CPU和内存激增

    top 指令查看 显示我的java进程 占用200%的CPU和30G的虚拟内存

    虽然我是Android开发 但是同时负责推送服务器的维护,仅仅是维护 平时就加些功能 新接入加厂商推送之类 并没有悉知的读过代码

    这次异常 不得不让我从头过一遍源码

    解决CPU占用异常

    重头过一遍源码后 每次下发厂商推送消息都会从数据库查一下该用户绑定的厂商推送信息

    如果每次推送都查询数据库 肯定是不行的 于是加了个缓存

    ConcurrentHashMap 做了个缓存队列 将用户id和厂商推送信息绑定

    当要下发推送时先找缓存队列里有没有 有就直接从缓存里拿 没有再查 查到之后加到缓存队列里

    按照这个思路修改后 重新部署到服务器

    CPU降下来了 从 200% 降到了 30%-40%左右

    这个肯定还有待优化的地方 但是目前问题已经解决

    解决虚拟内存占用过高

    接下来就是内存占用过高的

    经过实时监控发现 刚启动程序虚拟内存只占用了 900M

    紧接着已肉眼可见的速度增长 当涨到40G也就是分配的磁盘空间时 就产生OOM

    为什么会 内存溢出 因为系统已经没有内存分配给这个程序

    也就是说 程序里什么线程或者对象没有释放掉 导致一直占用内存 也就是内存泄漏 最终导致内存溢出

    想到这里 我先用

    jps -l
    
    • 1

    查看程序的进程的 pid

    然后 用

    jmap -histo:live pid > ./livelog.txt
    
    • 1

    打印了 当前进程下 内存的实际占用情况

    发现并没有什么异常 整个进程数据占用的内存只有400M左右

    接下来 使用

    jstack -l pid > ./stacklog.txt
    
    • 1

    打印堆栈信息

    不看不知道 一看吓一跳

    好家伙 3000个苹果推送的线程 锁死 和 4000个google推送的线程 锁死

    并且还在不断上涨 也就是说 每发一条google推送 或 苹果推送 就会生成一个线程来执行这个任务

    经过进一步观察 苹果的还好 APNS的库 用的okhttp3 我将 OkHttpClient 单独提出来 所有请求公用一个 OkHttpClient 避免的不停的创建

    也就是说 之前每发一条苹果推送 就会创建一个 OkHttpClient 而默认超时是60秒 如果网络波动或者发送异常 就会锁死60秒 等 OkHttpClient 超时后 才会释放掉这个 线程 而我们每秒有几百条请求 所以导致不断积累

    不过这些比起来google推送的几万条就是小巫见大巫了

    苹果推送的解决了 接下来是 google推送的

    和苹果推送同样的策略 但是我发现google推送是已经分装好的

    FirebaseMessaging messaging = FirebaseMessaging.getInstance(app);
    messaging.send(msgObj);
    
    • 1
    • 2

    使用 FirebaseMessaging 直接 send 要下发的消息即可 所以无从下手

    再梳理一番发现 由于我们国内的阿里云无法访问国际互联网 导致

    google推送鉴权失败 也就是说 每下发一条 google推送 如果没有鉴权 会先去鉴权 由于无法访问google导致鉴权耗时60秒 在这期间 线程会锁死 这还没玩 当google的库发现鉴权超时 会直接报错 但是不知道为什么 FirebaseMessaging无法释放 导致 线程一直锁死

    也就是说 google的推送会一直占用 并不会像苹果的推送一样 超时会释放

    怎么解决呢 我发现google的库版本太低 是不是库本身的bug 我决定将库提升到最新版

    升级了 google 的库 从6.0.0 升级到 目前9.1.1的版本

    
        com.google.firebase
        firebase-admin
        9.1.1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    重新测试 发现果然是库的问题

    最新版的库 也就是60秒超时后 可以正常释放 也就取消了线程的占用

    但是还是会锁死60秒 这个不能忍

    索性 国内禁用掉google推送 反正也收不到 开启没啥P用啊

    直接在推送服务器启动前 先判断 能否访问 google 的 80端口

    如果能访问 说明在国外 正常初始化 google推送的sdk

    如果不能访问 说明在国内 则禁用google推送

    public static boolean isOnline() {
        Socket server;
        try {
            server = new Socket();
            InetSocketAddress address = new InetSocketAddress("www.google.com", 80);
            server.connect(address, 3000);
            System.out.println("google 可用!");
            return true;
        } catch (UnknownHostException e) {
            System.out.println("google 解析异常!");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("google 连接超时!");
            e.printStackTrace();
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    重新打包 上线测试

    这波真万事大吉 虚拟内存从20G降到9000M

    经过一天的测试 发现没有虚拟内存一直保持在正常范围内 没有暴涨 CPU也正常 打印堆栈信息 也没发现线程锁死

    至此 该事故已完美解决

    最后

    当然 该系统还有待优化的地方 但这都是后话了。

    对于一个做Android的 初入后端 这次事故排查让我学到了很多

    这次从 线程池 内存泄漏 内存溢出 高并发 等 收获颇丰

    我这篇文章 仅仅是展示解决思路 公司代码也不能贴出 所以多多包涵

    大家如果遇到类似的问题 可以先看 我上面贴出的参考资料

    这些资料帮了我很多(虽然在大量的复制粘贴文章里感觉就像是 屎里淘金)

    可以先看看这些参考文章 再看看我的解题思路 应该有所帮助!

    报错处理

    当你使用 jmap -heap pid 出现如下报错

    参考-Java内存相关的常用命令

    Type "GenericGrowableArray", referenced in VMStructs::localHotSpotVMStructs in the remote VM, was not present in th
    e remote VMStructs::localHotSpotVMTypes table (should have been caught in the debug build of that VM). Can not continue.
    
    • 1
    • 2

    这是由于你的默认JAVA_HOME和程序使用Java的版本不一致导致的

    可以使用绝对路径 来访问

    C:\Users\itloser\.jdks\openjdk-17.0.1\bin\jmap -heap 6672
    
    • 1

    接下来你可能会遇到

    -heap option used
    Cannot connect to core dump or remote debug server. Use jhsdb jmap instead
    
    • 1
    • 2

    这表示你使用的Java版本是8.0以后的需要使用

    参考-查看jvm用到的一些命令

    C:\Users\itloser\.jdks\openjdk-17.0.1\bin\jhsdb jmap --heap --pid 6672
    
    • 1
  • 相关阅读:
    C#(三十八)之StreamWriter StreamWriter使用方法及与FileStream类的区别
    《跟我一起学“网络安全”》——等保风评加固应急响应
    java执行shell命令,Runtime.exec()和jsch谁更有优势?
    我在Vscode学OpenCV 几何变换(缩放、翻转、仿射变换、透视、重映射)
    php的html实体和字符之间的转换
    harmonyOS鸿蒙开发工具下载安装以及使用流程
    【每日一题】1498. 满足条件的子序列数目
    [跨数据库、微服务] FreeSql 分布式事务 TCC/Saga 编排重要性
    第三章 神经网络——什么是神经网路&激活函数&3层神经网络的简单实现&手写数字识别
    使用Cpolar和Tipas在Ubuntu上搭建私人问答网站,构建专业问答系统
  • 原文地址:https://blog.csdn.net/qq_38376757/article/details/127903191