某线上应用在进行查询结果导出Excel时,大几率出现持续的FullGC。解决这个问题时,记录了一下整个的流程,也能够做为通常性的FullGC问题排查指导。html
为了定位FullGC的缘由,首先须要获取heap dump文件,看下发生FullGC时堆内存的分配状况,定位可能出现问题的地方。java
能够在JVM参数中设置-XX:+ HeapDumpBeforeFullGC
参数。
建议动态增长这个参数,直接在线上镜像中增长一方面是要从新打包发布,另外一方面风险比较高linux
sudo -u admin /opt/taobao/java/bin/jinfo -flag +HeapDumpBeforeFullGC pid
sudo -u admin /opt/taobao/java/bin/jinfo -flag +HeapDumpAfterFullGC pidapache
也能够用HeapDumpOnOutOfMemoryError这个参数,只在outOfMemoryError发生时才dump。实测只有在fullgc完成时才会产生该文件,fullgc期间看不到。
此外还须要-XX:HeapDumpPath=/home/admin/logs/java.hprof
这个参数来指定dump文件存放路径。json
先获取java进程ID,再使用jmap进行dump。
注意,虚拟机上的jmap可能没有作路径映射,须要手动选择jdk路径下来执行api
ps -aux | grep java jmap -dump:file=test.hprof,format=b XXXX
JDK7后新增的多功能命令,其中jcmd pid GC.heap_dump FILE_NAME
的效果和jmap -dump:file=test.hprof,format=b pid
同样。浏览器
能够生成本机或远程JVM的dump。还有一些其余工具就不详细介绍了。服务器
因为使用的是阿里云的服务器,能够直接将dump文件上传到OSS上经过公司内部工具来分析,或经过OSS再下载到本地。
设置OSSCMD:
操做命令 osscmd config --host=oss-cn-hangzhou-am101.aliyuncs.com --id=** --key=**
建立bucke:osscmd cb 000001
上传文件:osscmd put 1.txt oss://000001/
下载文件:osscmd get oss://000001/1.txt 1.txtapp
其余类型的Linux主机可使用SCP命令,参考:Linux scp命令框架
经过dump文件来分析fullGC的缘由,须要关注哪些类占用内存空间较多、不可到达类等。
因为使用的是公司内部工具Zprofiler和grace,详细的使用过程这里就不截图了。一些其余可用的工具和命令(参考Java内存泄漏分析系列之六:JVM Heap Dump(堆转储文件)的生成和MAT的使用):
jhat <heap-dump-file>
生成网页,经过浏览器访问``查看须要注意的是,只看dump文件有时还不能获得结论,由于占用空间大头的有多是String、ArrayBlockingList这样的对象,并且内容多是null或null对象的集合,无从排查。此时还要结合发生fullgc先后业务系统发生了什么动做来肯定。若是有条件的话能够在平常环境或预发环境重现一下。
固然,若是内存中的空间消耗对象是特殊的类,就比较好排查了。
具体状况具体分析。
查询DB中数据->在异步线程中经过poi转换成Excel->上传到OSS。
示例代码:
// 导出代码中将变量直接做为lambda表达式的值传入 List<XXData> data = queryData(request); SheetDownloadProperty property = sheetDownloadProperties.get(0); property.setTotalCount(request.getQueryRequest().getPageSize()); property.setPageSize(request.getQueryRequest().getPageSize()); property.setQueryFunction((currentPage, pageSize) -> data); // 该组件会在线程池异步调用poi组件转换为excel、上传OSS、下载 asyncDownloadService.downloadFile(downloadTask);
private List<XXData> queryData(ExportRequest request) { //查询DB,略 }
// 查询方法 @FunctionalInterface public interface PageFunction<T> { /** * 方法执行 */ List<T> apply(Integer currentPage,Integer pageSize); }
经过内部工具可见,fullGC前有三个占据内存较高的ArrayBlockingList,里面有大量的内容为null的Object。
这三个ArrayBlockingList所属的中间件,虽然自己和业务流程没有关系,可是仍不能排除嫌疑。
因为依赖了二方库poi,这个库的usermodel模式很容易引发fullGC,同时也怀疑是由于lambda表达式直接传了变量。
把poi的usermodel改成事件模式(https://my.oschina.net/OutOfMemory/blog/1068972)能够避免这个问题。
可是该功能是一个二次封装的三方包中的,同时其余引用该组件的应用fullgc频率并不高,没有采用这个方案。
持有大量null对象的中间件版本较低,且新版目前已再也不维护,老版本的releas note虽然没有提到这条bug fix,有必定嫌疑。
该中间件初始化时会建立三个容量为810241024的ArrayBlockingList,和dump文件相符合。
一样是由于这个中间件是在三方包中封装,不方便直接该版本,一样没有采用这个方案。
能够调整metaspace参数来实现,本次想找到代码中相关的线索来解决,未采用该方案。
仔细观察了这段代码在其余系统的的实现,发现其余系统的lambda表达式是匿名方法,而不是直接传值,即:
property.setQueryFunction((currentPage, pageSize) -> { // 查询逻辑, 略 );
怀疑是直接传变量进去致使的垃圾回收问题。更改到这种模式后,触发下载功能时,连续长时间的fullGC仍然时有发生,没有解决问题。
暂时能肯定的缘由是,公司中间件自己占用堆内存较多,运行poi增长了GC的频率。可是因为它们都在二方库的缘由,不方便修改。
此时搜索到stackoverflow有关于poi反复GC的一个问题,和个人状况相似,也是反复GC可是仍然不能释放内存。有回复建议将GC回收器替换为G1GC,将默认的UseConcMarkSweepGC替换后效果明显,一次FullGC就能够完成回收释放,不会反复FullGC,以下图,20:30前的fullGC是CMS,持续时间长且反复进行;20:30后是替换后第一次触发excel转换下载,进行了屡次下载,即便发生FullGC也只有1次,大大缓解了以前的问题:
本次暂定只采用方案5。
G1GC在JDK9已替代CMS成为了正式的垃圾回收器,低版本JDK须要手动设置。具体须要设置的JVM参数:
-Xms32m -Xmx1g -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:MaxHeapFreeRatio=15 -XX:MinHeapFreeRatio=5
注意前两行通常应用都会设置,不要覆盖掉。最后两行须要视状况调整。另外,默认的-XX:+UseConcMarkSweepGC
须要去掉。
使用G1GC时须要确认工做线程数是否和预期一致,不要太多,通常来讲和CPU核数一致便可。出现非预期数目的缘由多是,镜像脚本指定核数时,直接按照物理机而不是虚拟机核数来生成。
查看方式是看gc日志:
虚拟机设置核数的dokcker脚本示例:
export CPU_COUNT="$(grep -c 'cpu[0-9][0-9]*' /proc/stat)"
core dump是针对线程某一时刻的运行状况的,能够看到执行到哪一个类哪一个方法哪一行以及执行栈的;heap dump是针对内存某一时刻的分配状况的。
简单摘译了一些,能够直接看原文。
关于G1GC,会在后续文章中研究。