内存泄露排查之线程泄露

若是只关心具体过程,可直接回归正途的处理逻辑
原文连接:http://www.javashuo.com/article/p-plzezhmt-cv.htmlhtml

基础

内存泄露(Memory Leak)

  1. java中内存都是由jvm管理,垃圾回收由gc负责,因此通常状况下不会出现内存泄露问题,因此容易被你们忽略。
  2. 内存泄漏是指无用对象(再也不使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而形成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,须要自主观察,比较严重的时候,没有内存能够分配,直接oom。
  3. 主要和溢出作区分。

内存泄露现象

  • heap或者perm/metaspace区不断增加, 没有降低趋势, 最后不断触发FullGC, 甚至crash.
  • 若是低频应用,可能不易发现,可是最终状况仍是和上述描述一致,内存一致增加

perm/metaspace泄露

  • 这里存放class,method相关对象,以及运行时常量对象. 若是一个应用加载了大量的class, 那么Perm区存储的信息通常会比较大.另外大量的intern String对象也会致使该区不断增加。
  • 比较常见的一个是Groovy动态编译class形成泄露。这里就不展开了

heap泄露

比较常见的内存泄露
  1. 静态集合类引发内存泄露
  2. 监听器:但每每在释放对象的时候却没有记住去删除这些监听器,从而增长了内存泄漏的机会。
  3. 各类链接,数据库、网络、IO等
  4. 内部类和外部模块等的引用:内部类的引用是比较容易遗忘的一种,并且一旦没释放可能致使一系列的后继类对象没有释放。非静态内部类的对象会隐式强引用其外围对象,因此在内部类未释放时,外围对象也不会被释放,从而形成内存泄漏
  5. 单例模式:不正确使用单例模式是引发内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),若是单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,致使内存泄露
  6. 其它第三方类

本例(线程泄露)

本例现象

  1. 内存占用率达80%+左右,而且持续上涨,最高点到94%
    内存占用java

  2. yongGC比较频繁,在内存比较高的时候,伴有FullGC
    gc次数算法

  3. 线程个个数比较多,最高点达到2w+(这个比较重要,惋惜是后面才去关注这点)
    线程数据库

  4. 日志伴有大量异常,主要是三类
    • fastJosn error
      fastJson错误.安全

    • 调用翻译接口识别语种服务错误
      翻译服务网络

      翻译错误代码

    • 对接算法提供的二方包请求错误
      predict错误多线程

      算法调用错误

刚开始走的错误弯路

  1. 刚开始发现机器内存占用比较多,超过80%+,这个时候思考和内存相关的逻辑
  2. 这个时候并无去观察线程数量,根据现象 一、二、4,、这个过程没有发现现象3,排查无果后,从新定位问题发现现象3
  3. 因为现象4中的错误日志比较多,加上内存占用高,产生了以下想法(因为本例中不少服务经过mq消费开始)
    • 现象4中的错误致使mq重试队列任务增长,积压的消息致使mq消费队列任务增长,最终致使内存上升
    • 因为异常,逻辑代码中的异常重试线程池中的任务增长,最终致使任务队列的长度一直增长,致使内存上升

解决弯路中的疑惑

  • 定位异常
    • fastJson解析异常,光看错误会以为踩到了fastJson的bug(fastJson在以前的版本中,写入Long类型到Map中,在解析的时候默认是用Int解析器解析,致使溢出错误。可是这个bug在后面的版本修复了,目前即便是放入Long类型,若是小于int极限值,默认是int解析,超过int极限,默认long。类中的变量为Long。直接parse,直接为Long类型),可是业务代码中使用的是类直接parse,发现二方包中的类使用了int,可是消息值有的超过int值
    • eas算法链路调用错误,以前就有(404),可是没有定位到具体缘由,有知道的望指点下,这里用try catch作了处理
    • 翻译服务异常,这里没定位到具体缘由,重启应用后恢复,这里忘记了作try catch,看来依赖外部服务须要所有try下
  • 确认是不是业务逻辑中错误重试队列问题
    • 否,和业务相关才会走入重试流程,还在后面
  • 确认是不是Mq消息队列积压,以及Mq重试队列消息积压致使,确认是不是线程自动调整(metaq/rocketmq)
    • 否,Mq作了消费队列安全保护
    • consumer异步拉取broker中的消息,processQueue中消息过多就会控制拉取的速率。对于并发的处理场景, 存在三种控制的策略:
      1. queue中的个数是否超过1000
      2. 估算msg占用的内存大小是否超过100MB
      3. queue中仍然存在的msg(多半是消费失败的,且回馈broker失败的)的offset的间隔,过大可能表示会有更多的重复,默认最大间隔是2000。
    • 流控源码类:com.alibaba.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage,圈中的变量在默认的类中都有初始值
      流控源码
  • metaq也会本身作动态线程调整,理论上当线程不够用时,增长线程,adjustThreadPoolNumsThreshold默认值10w,当线程比较多时,减小线程,可是代码被注释了,理论上应该没有自动调整过程,因此这里也不会由于任务过多增长过多线程
    • 在start启动的时候,启动了一批定时任务
      mqStart并发

    • 定时任务中启动了调整线程的定时任务
      启动定时调整异步

    • 启动调整任务
      调整
      调整具体代码.jvm

回归正途的处理逻辑

  • 通过上述分析,发现并非由于异常致使的任务队列增长过多致使,这个时候,发现了现象3,活动线程数明显过多,确定是线程泄露,gc不能回收,致使内存一直在增加,因此到这里,基本上就已经确认是问题由什么致使,接下来要作的就是确认是这个缘由致使,以及定位到具体的代码块
  • 若是没有具体的监控,通常就是看机器内存情况,cpu,以及jvm的heap,gc,有明显线程情况的,可jstack相关线程等,最终依然没法定位到具体代码块的能够dump后分析
登陆涉事机器
  • top,观察内存占用率(这里图是重启以后一段时间的)可是cpu占用率比较高,很快就降下去了,这里耽误了一下时间,top -Hp pid,确认那个线程占用率高,jstack看了下对应的线程在做甚
    top

  • 确认线程是否指定大小,未发现指定,使用的默认值大小

    gc参数

  • 查看heap,gc情况
  • 查看线程情况,可jstack线程,发现线程较多,也能定位到,可是为了方便,遂dump一份数据详细观察堆栈
    • 线程个数
      • cat /proc/{pid}/status (线程数居然这么多)
        命令行线程个数

      • 因为线程数比较多,而依然能够建立,查看Linux普通用户所容许建立的进程数,使用命令:cat /etc/security/limits.d/90-nproc.conf ,值比较到,远超当前的个数

    • 线程信息
      线程个数

    • 线程状态
      线程状态

    • 定位到问题线程
      • AbstractMultiworkerIOReactor ==》 httpAsycClient ==》如图所示不能直接定位到代码块,因此maven定位引用jar的服务 ==> 具体二方包
    • 若是每次都new线程而不结束,gc中线程是root节点,若是线程没有结束,不会被回收,因此若是建立大量运行的线程,会致使内存占用量上升,可是线上到底能建立多少线程呢?

    • 问题代码块
      • 方法开始(每次都初始化一个新的客户端,底层封装使用httpAsyncClient,httpAsyncClient使用NIO模型,初始化包含一个boss,10个work线程)
        方法开始

      • 方法结束(方法结束都调用了shutdow)
        方法结束

    • 根据现象和对应线程堆栈信息,能肯定线程就是在这边溢出,客户端的shutDown方法关闭线程池失效,致使因为初始的线程都是NIO模式,没有被结束,因此线程一直积压增长,可修改成单例模式,限制系统使用一个线程池,上线后解决问题

httpAsyncClient 部分源码解析
  • 启动
    • 常驻线程
      • Reactor Thread 负责 connect
      • Worker Thread 负责 read write
    • http启动线程
    • 线程池命名,也就是上面出现pool--thread-的线程
      普通线程池命名
    • ioEventDispatch 线程
      • 启动
        启动
      • worker线程
        worker线程
      • worker线程名称
        worker线程名称
      • IO worker运行详细
      • worker线程实现


  • shutdown 这里就不作分析了,调用后,线程都会跳出死循环,结束线程,关闭连接等好多清理动做
疑问
  • 虽然每次方法调用都是new新的客户端,可是结束finally中都调用了shutDown,为什么会关闭失败,上面使用单例模式,只是掩盖了为何每次new客户端而后shutdown失效的缘由
  • httpAsyncClient客户端在请求失败的状况下,httpclient.close()此处会致使主线程阻塞,经源码发现close方法内部,在线程链接池关闭之后, httpAsyncClient对应线程还处于运行之中,一直阻塞在epollWait,详见上面的线程状态,这里目前没有肯定下为何调用shutdown以后线程关闭失败,也没有任何异常日志,可是这是致使线程泄露的主要缘由
  • 在本地测试shutdown方法可正常关闭,非常奇怪。若是各位有知道具体的缘由的,望指教
  • 原文连接
相关文章
相关标签/搜索