记一次EasyExcel的错误使用导致的频繁FullGC

2024-07-08 1515阅读

记一次EasyExcel的错误使用导致的频繁FullGC

      • 一、背景描述
      • 二、场景复现
      • 三、原因分析
      • 四、解决方案
      • 五、思考复盘

        一、背景描述

        繁忙的校招结束了,美好的大学四年也结束了,作者也有10个月没有更新了。拿到心仪的offer之后也开始了苦B的打工生活。

        最近接到了这样的一个需求:从大量Excel文件中清洗出来关键信息,文件数量很多,数据量也很大。

        早就听说EasyExcel是处理Excel的利器,性能极高的同时还不会出现内存溢出,作者想都没想就开始用了起来,于是就有了今天这篇文章。。。。

        二、场景复现

        参照GPT以及一些文档还有以前的一点点使用经验,作者写了这样一段代码。

        @Component
        public class EasyExcelUtil {
            // 这里开了32个线程
            @Async("excelExecutor")
            public void test(String fileName){
                ExcelReaderBuilder read = EasyExcel.read(fileName);
                List objects = read.doReadAllSync();
                // 其他处理逻辑
            }
        }
        

        观察了一会日志,发现运行的还挺正常,作者就心满意足的去写文档了,悲剧的是写完文档回来发现,GC日志上面疯狂的FullGC,文件也只处理了一千个左右,当时的心情是极其复杂的,于是就开始了漫长的排查。

        三、原因分析

        首先观察日志,这时候有些文件其实还是被处理了的,频繁的FullGC日志中有一些年轻代是被正常回收了的,但是老年代已经满了,且无论怎么回收,都不会被回收掉,这时候其实就可以想到一种可能性是有一些不会被FullGC回收的大对象存在。于是我去dump了堆内存图,老年代的分布大概是这样的:

        记一次EasyExcel的错误使用导致的频繁FullGC

        其中SyncReadListener的对象躲过了所有的FullGC且没有GC Root,猜测一定是SyncReadListener这个类出现了什么问题,我们先看doReadAllSync()这个方法的源码

        记一次EasyExcel的错误使用导致的频繁FullGC

        可以看到是先注册了SyncReadListener这个监听器,然后构造了一个excelReader对象,通过excelReader对excel进行读取,那为什么SyncReadListener会出现这么多大对象呢,我们看看源码。

        记一次EasyExcel的错误使用导致的频繁FullGC

        SyncReadListener可以将某些数据一条条的塞进去,这里我们合理推测其实就是我们读取到的数据被传递给了监听器,但是为什么没有被垃圾回收掉呢?推测问题应该就出现在了ExcelReader这个类。

        首先是常量定义和一些读取的方法。

        记一次EasyExcel的错误使用导致的频繁FullGC

        接下来这部分内容就有意思了,也是问题所在。

        记一次EasyExcel的错误使用导致的频繁FullGC

        这个类重写了finalize方法,调用了一次finish()方法,而刚才的代码中调用的逻辑是这样的

        excelReader.readAll();
        excelReader.finish();
        

        具体的逻辑就不细看了,语义上的描述大概是读取所有的内容,然后手动关闭。

        这时候就真相大白了,结合我们的代码中又添加了@Async注解,场景发生的原因大概是:

        多个线程同时读取到了超大文件,导致在excelReader.readAll()过程中老年代被打满,老年代已经没有空间去读取这几个超大文件中的内容了,且由于ExcelReader重写了finalize()方法,并不会进入到GC队列,这就会导致老年代的占用一直是接近100%,不断的触发FullGC,而那些使用年轻代就能进行读取的小文件就可以正常的进行数据解析,随后被GC掉。

        四、解决方案

        学习了官方文档后,发现作者的场景应该使用这部分逻辑,即继承AnalysisEventListener,重写invoke方法,doAfterAllAnalysed()方法,最关键的是定义一个没读取一部分数据就释放空间的List,这样可以实现读取一部分内容后就释放内存,不会出现读取超大文件导致大对象无法回收的问题,也是这个工具类的正确使用方法。

        记一次EasyExcel的错误使用导致的频繁FullGC

        五、思考复盘

        1. 选择某个工具类实现功能的时候一定要充分阅读文档,找到自己需要的能力
        2. 学习JVM,这会让好多排查过程变得非常轻松
        3. 养成阅读源码的习惯,快速定位生产中的问题
        4. 学习设计模式,哪怕自己的屎山没机会通过设计模式重构,也能提高自己阅读优秀开源组件实现逻辑的能力

        最后感慨一下:用EasyExcel这个组件好长时间了,都没有去探索他的实现逻辑,而且最开始使用EasyExcel真的是觉得他用起来比POI更加的Easy,根本不了解他可以解决内存溢出的问题,更是忽略掉了这个组件的更加牛逼的用途,自己的成长空间还是很大啊。。。

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]