内存溢出分析-MAT使用

346 阅读4分钟

一次生产问题分析定位

记一次帮其它团队排查生产问题的过程,顺带结合该场景讲一下怎么用MAT分析内存,以及开发过程中的一些规范问题。由于重启前没有用jstack、jmap命令保存jvm的内存信息,应用的日志打印也不规范,导致排查比较艰难。

问题定位过程

问题发生时间:

  • a. 下午发现生产大量交易超时,排查发现依赖的一个被很多交易依赖的下游接口超时
  • b. 服务重启后问题暂时恢复,但当天未排查到原因
  • c. 第二天参与排查

排查定位过程:

  • a. ELK上看不到该应用日志,团队也说没拿到日志。先是团队反馈应用日志报错拿不到“数据库连接”,截图只有如下内容(后续看到这个截图截了一半有点害人)。

    :Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool
    

    由于我们应用并发并不高,第一反应以为是有长耗时Sql或者是大事务导致链接无法释放,最终链接不够用。检查代码,发现用到事务的代码只有一个地方,且只有简单的插入、更新操作,DBA反馈当时数据库也没有慢Sql。问题一时有点无头绪

  • b. 拿到应用侧出错时间段日志后,搜索Exception关键字,发现一小时有上千条异常日志,对异常排查,未发现会引起连接池不够的异常(异常太多,我也没仔细检查,导致重要异常被忽略。后续了解这些异常正常交易时间段也会报,应该用warn或者info级别)。

  • c. 查看其中CannotGetJdbcConnectionException异常,查看报错:

    :Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 17276, active 7, maxActive 200, creating 1, createElapseMillis 17795
    

    其中有active 7,说明不是链接池满了,而是链接不够用无法创建新链接。和DBA确认当时数据库无异常,检查链接数也无问题;检查linux服务器涉及线程数的配置也都足够。由于当时重启时未拿到jvm信息,暂时也没了方向(其实无法创建链接,这时应该怀疑jvm内存问题,可以检查一下是否有内存溢出问题)

  • d. 经检查,发现日志中有"java.lang.OutOfMemoryError: Java heap space"报错。启动脚本配置有内存溢出则自动保存内存堆栈,检查机器上确实有堆栈文件。这样问题基本清晰,就剩下分析该文件了定位内存溢出原因了

内存溢出问题分析

我用的是MAT工具,其中的一些参数,Shallow Heap:浅堆,指一个对象所消耗的内存(不包括内部引用的对象大小);Retained Heap:深堆,某对象保留集中所有对象的浅堆之和;outgoing reference:当前对象引用的外部对象集合;incoming references:引用当前对象的外部对象集合。

  • a.首先Leak Suspects页面,有可能存在的内存泄漏。代码质量问题,有很多同名线程池,无法定位问题。

    leakSuspect
  • b. Histogram视图。能看到占用内存最大的是char[]对象 histogram

  • c. 点击char[]->右键->List objects->with incoming references。按照Reatained Heap排序。这时可以看到占内存大的char数组中的具体信息,根据字符串格式、类名可以定位问题。(能看到一部分是日志,一部分是对象的序列化)

    histogramIncoming
  • d. dominator_tree,查看大对象。同样看到线程里都是打印日志的字符串

    dominatorTree

现在基本可以定位是日志打印大对象,导致内存溢出,无法创建新的数据库连接,服务不可用。根据日志字符串中的类名,在idea检索引用,最终查到是一个2万大小的对象数组在多个线程中log打印序列化,导致了该问题。

总结

通过该问题排查过程,可以看到,在开发及碰到生产问题后,以下几点有助于我们快速定位问题

  • a. 为快速恢复生产,重启前通过jstack、jmap等命令打印jvm内存信息
  • b. 规范error日志,只有真正需要排查的问题才能是error级别
  • c. 线程池的命名应该易辨识