POI导出导致内存溢出排查

  • Post author:
  • Post category:其他


最近出现一个线上问题,就是导出之后内存使用率激增,并且迟迟释放不掉,导致服务挂掉。首先声明一下出现问题的场景

系统的订单导出,订单字段较多,数据量较大。

产生这个问题的时候第一时间去看了代码

                    orderService.listOrders(objectToMaps, uid, request, startDate, endDate, page, size, permission, organizationNodeIds,true);
                    if (objectToMaps.isEmpty()) {
                        break;
                    }
                    Workbook localWorkbook = null;
                    if (exportFile.exists()) {
                        try {
                            localWorkbook = ExcelUtil.load(new FileInputStream(exportFile));
                        } catch (InvalidFormatException e) {
                            log.error("error:{}", e);
                        } catch (IOException e) {
                            log.error("read excel failed, error:{}", e);
                        }
                    }
                    HSSFWorkbook workbook = null;
                    if (request.getType() == 3) {
                        workbook = exportReceiveOrderToWorkbook(uid, operator, objectToMaps, request, startDate, endDate, localWorkbook, count);
                        count += objectToMaps.size();
                    } else {
                        if (withCargoes) {
                            List<String> orderIds = objectToMaps.parallelStream().map(o -> (String) o.get("id")).collect(Collectors.toList());
                            Map<String, List<TtmsOrderCargoEntity>> orderCargoes = ttmsOrderModuleService.findAllOrderCargoesByOrderIds(orderIds);
                            final int[] totalOrders = {objectToMaps.size()};
                            objectToMaps.forEach(order -> {
                                String orderId = (String) order.get("id");
                                List<TtmsOrderCargoEntity> cargoes = orderCargoes.get(orderId);
                                if (!CollectionUtils.isEmpty(cargoes)) {
                                    order.put("orderCargoes", cargoes);
                                    totalOrders[0] += cargoes.size() - 1;
                                }
                            });
                            workbook = exportOrdersWithCargoesToWorkbook(uid, operator, objectToMaps, request, startDate, endDate, localWorkbook, count);
                            count += totalOrders[0];
                        } else {
                            workbook = exportCustomerOrdersToWorkbook(uid, operator, objectToMaps, request, startDate, endDate, localWorkbook, count);
                            count += objectToMaps.size();
                        }
                    }
                    if (workbook != null) {
                        log.info("save export file to {}", exportFile.getAbsolutePath());
                        try {
                            FileOutputStream fos = new FileOutputStream(exportFile);
                            workbook.write(fos);
                            fos.close();
//                          workbook.write(fis);
                        } catch (IOException e) {
                            log.error("error:", e);
                        }
                    }
                    objectToMaps.clear();
                    page++;
                    if (page >= MAX_PAGES) {
                        break;
                    }
                }
然后发现做法是分批次查询,查询之后写入文件然后将文件写入本地,然后再读进来,将数据增量写入,这样做的初衷可能是为了提高效率,但是确为后面埋下了一个深坑。
内存占用率一直居高不下,并且如果说短时间内多做几次导出的时候导致的是永久代内存溢出。本地设置的1G的JVM,但是在开始导出的时候内存从200+M直线飙升到700M左右

上面是正常的情况下本地的内存

打开visualVM大致看一下gc的回收频率,发现这段时间GC在频繁的youngGC,初步猜测是导出的时候查询的数据量比较大,但是后来改为两百条查一次,还是没有变动,排除数据问题

这个时候猜测是poi提供的API的问题,而且这个时候youngGC之后内存一直没有减少,一直保持在增长之后的水平,并没有GC掉很多对象,初步猜测是因为对象太大直接进入老年代。这个时候导出已经完成了,再看内存,依然是保持在刚才的水平,过了大概五分多钟之后,来了一次FullGC,内存直接从700降到300左右,初步判断是因为老年代GC需要进行多次标记(GC回收算法)。

在修改代码之前发现用的是HSSFWorkBook,后来去看了一下说明,HSSFWorkbook只能操作excel2003一下版本,XSSFWorkbook只能操作excel2007以上版本,而且看了很多网上说的方式,但是并没有任何解决方案。

从POI 3.8版本开始,提供了一种基于XSSF的低内存占用的API—-SXSSFWorkBook,所以我们就用了这个版本的来替代以前旧版本的API,但是问题好像并没有解决,反而更加严重了。

那么我猜测是新版本的API不适合这样的操作。后来去看了一下文档,发现最新版本的SXSSFWorkBook适合大量数据操作而且不会占用太大的内存,那么为什么我们用的时候依然会出现这样的问题呢?用法不对?是的,没错。

以前的时候为了效率我们采用先创建写入数据,然后保存文件,然后读文件增量写入,如此往复。但是这样的可能适合以前的版本,并不适用于新版本的API,开始行动,不保存文件,分批次查询数据,分批次写入SXSSFWorkBook。跑起来之后发现内存的使用率是有所上升,而且youngGC的频率很高,但是并没有出现内存直线飙升的现象。而且在波动之后内存回归正常,并没有出现内存一直释放不掉只能等FullGC的情况。所以还是用法的问题,还是需要多看文档

回顾一下问题,为什么之前版本的会一直常驻在内存等待FullGC呢?猜测是因为以前版本的API占用内存确实较大,对象过大直接进入老年代,而老年代的GC回收频率比不上年轻代,所以GC需要去标记判断很多次才会把对象回收掉。而且另外一点就是我们的做法,创建对象,写出到文件,读文件,写入数据,写出文件 ,读文件……如此往复更加是雪上加霜。所以此次的问题主要还是代码待优化,没有理解官方给出的用法

如果有理解上的错误,还有待各位大神提出见解



版权声明:本文为Sean_gsy原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。