JVM调优-内存泄露排查
背景
在pinpoint上观察到一些服务器频繁发生长时间FULL GC,直接影响业务执行效率。
观察gc日志发现,老年代几乎已经耗尽,也没有发生Promotion Failed、Concurrent Mode Failure,最有可能的情况是年轻代正常晋升时发现老年代的空间已经达到了-XX:CMSInitiatingOccupancyFraction
,所以触发老年代回收以释放更多空间。
为此,可以采用的解决方式几乎只有2个:
- 物理增大老年代,可以直接把年轻代空间割让给老年代,实际上上图中年轻代:老年代≈2:1不大合理,一般根据实际情况,年轻代:老年代的比值是1:1.5到1:3都是可以的,但是不会出现年轻代比老年代大的情况,除非绝大多数对象都是朝生夕死的。
- 排查内存泄露,常见于一些错误配置、或者本地缓存(一个大Map)之类的。
排查方式
一、直连VisualVM
1、配置jmx启动参数,表示允许远程连接
1 | -Dcom.sun.management.jmxremote |
2、使用VisualVM连接
直连有两个缺点:
- 实时性差,问题发生可能就那么几分钟,不可能让人一直等着服务器出问题;
- 安全性差,暴露一个端口就意味着暴露整个项目及其连接的数据库,是一个隐患。
二、配置参数实时导出
1、导出位置-XX:HeapDumpPath=$CATALINA_BASE/logs/
2、FULL GC前DUMP-XX:+HeapDumpBeforeFullGC
3、FULL GC后DUMP-XX:+HeapDumpAfterFullGC
4、OOM-XX:+HeapDumpOnOutOfMemoryError
配完这个参数后,FULL GC后内存仍不足才会DUMP,单纯System.gc或FULL GC不会引起DUMP。
三、手动dump分析
到服务器上找到JVM进程PID后执行jmap:
1 | jmap -dump:format=b,file=<文件名> [pid] |
下载到本地,因为线上服务器没有开启下载端口,因此下载文件必须要中转一下,先上传到一台暴露到公网的服务器,再从该服务器上下载,如果没有这样的服务器也可以用github代替。
在本地使用eclipse MAT分析。
问题排查记录
手动导出内存dump文件:
仔细观察其中对象占用的内存空间,发现一个大对象占用了接近1/3的内存空间,发现是com.alibaba.druid.stat.JdbcDataSourceStat
中有个LinkedHashMap类型的成员变量占用了大量空间没有释放,在源码中对应一个sqlStatMap。
从源码可知,打开开关之后,每次执行SQL前后都会把SQL记到这个sqlStatMap里,在JVM实例运行期间会一直存在(基本都在老年代),这个开关主要用于监控SQL执行情况,对SQL语句的执行本身没有影响,且我们这边统计SQL(包括慢查询等)都是基于MySQL本身的功能、并没有用到这个配置。
可以把这个开关关掉,因为我们根本没有使用Druid内置的SQL监控功能,关闭后预计可以释放25%左右的堆空间。
另外附:
1、配置_StatFilter:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter
2、每个sql语句都会长期持有引用,加快FullGC频率 #1664:https://github.com/alibaba/druid/issues/1664