优化大量数据导出到Excel的内存消耗

导出数据,数据量太大,一直处于加载中,然后刷新页面,也刷新不出来。

原因:

1、在反序列化 Redis 数据时发生了问题,导致 Java 虚拟机耗费过多时间在垃圾回收上,最终触发了 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误。这可能是由于 Redis 存储的某些数据过大或者存储了大量的数据,导致在反序列化时内存使用过多,垃圾回收器无法及时清理。

2、java.lang.OutOfMemoryError: Java heap space 错误表示 JVM 的堆空间不足,无法满足程序运行的需求。这通常发生在应用程序试图分配更多内存而堆的可用空间已经耗尽的情况下。
程序试图使用更多的内存,但是系统当前可用的内存空间不足。这可能是由于系统负载较大或者应用程序需要处理大量数据导致的

解决措施

对于原因1可以调整垃圾回收设置:

使用并发垃圾收集器(Concurrent Garbage Collector)可以帮助减小垃圾回收对应用程序的影响,提高应用程序的性能。在Java虚拟机的启动参数中添加 -XX:+UseG1GC 参数来启用G1垃圾收集器。

修改启动脚本,将Java虚拟机的启动命令中添加 -XX:+UseG1GC 参数。例如:

java -Xms2G -Xmx3G -XX:+UseG1GC -jar your-application.jar

对于原因2:

解决措施(1):增加堆大小(本人部署应用的服务器内存总16GB,可用内存约4.3GB)

        ①将最大堆大小(-Xmx)设置为3GB,初始堆大小(-Xms)设置为2GB。--导到178585条数据时触发 
java.lang.OutOfMemoryError related to the Java heap space. Java 堆空间不足


        ②将最大堆大小(-Xmx)设置为4GB,初始堆大小(-Xms)设置为2GB。--21万可以正常导出。超过213214条数据时触发 java.lang.OutOfMemoryError related to the Java heap space. Java 堆空间不足

解决措施(2):优化代码

本文采用了缓冲写入

修改前代码:

 public void writeExcel(OutputStream os, String sheetName, Map<String, String> header, List<Map<String, Object>> datas)  {
        logger.info("导入数据到excel==========> start");
        long startTime = System.currentTimeMillis();  // 记录开始时间
        XSSFWorkbook wb = null;
        try {
            wb = new XSSFWorkbook();
            XSSFSheet sheet = wb.createSheet(sheetName);
            int rowNum = 0;
            XSSFRow row = sheet.createRow(rowNum);
            Map<String, CellStyle> cellstyles = initStyles(wb);
            int cellNum = 0;
            for (Map.Entry<String, String> entry : header.entrySet()) {
                String fieldDesc = entry.getValue();
                row.createCell(cellNum).setCellValue(fieldDesc);
                logger.info("导入数据到excel==========> header" + entry.getKey());
                cellNum++;
            }

            int totalRecords = ObjectKit.isNotEmpty(datas) ? datas.size() : 0;  // 总共导出记录数
            logger.info("总共导出记录数: " + totalRecords);

            if (ObjectKit.isNotEmpty(datas)) {
                for (Map<String, Object> map : datas) {
                    rowNum++;
                    row = sheet.createRow(rowNum);
                    logger.info("导入数据到excel==========> row====" + JSON.toJSONString(map));
                    cellNum = 0;
                    for (Map.Entry<String, String> entry : header.entrySet()) {
                        String fieldName = entry.getKey();
                        Object data = map.get(fieldName.toUpperCase());
                        String dataString = null == data ? "" : data.toString();
                        if (data instanceof BigDecimal) {
                            Cell cell = row.createCell(cellNum);
                            cell.setCellValue(((BigDecimal) data).toPlainString());
                            cell.setCellStyle(cellstyles.get("Number"));
                        } else {
                            if (data instanceof Date || data instanceof Timestamp) {
                                if (data.toString().contains(".")) {
                                    dataString = null == data ? "" : data.toString().substring(0, data.toString().indexOf("."));
                                } else {
                                    dataString = null == data ? "" : data.toString();
                                }
                            }
                            row.createCell(cellNum).setCellValue(null == data ? "" : dataString);
                        }
                        cellNum++;
                    }

                    // 当前已导出记录数及进度
                    logger.info("当前已导出记录数: " + rowNum + ", 进度: " + ((float) rowNum / totalRecords) * 100 + "%");
                }
            }
            logger.info("导入数据到excel==========> end");
            long endTime = System.currentTimeMillis();  // 记录结束时间
            long elapsedTime = endTime - startTime;  // 计算耗时时间
            logger.info("总共导出记录数: " + totalRecords);
            logger.info("耗时时间: " + elapsedTime + " 毫秒");

            wb.write(os);
        } catch (Exception e) {
            throw new ImpException(ImpError.APP_ERR_20_04_10, e);
        } finally {
            try {
                if (null != wb) {
                    wb.close();
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

修改后:

 public void writeExcel(OutputStream os, String sheetName, Map<String, String> header, List<Map<String, Object>> datas) {
        logger.info("导入数据到excel==========> 开始");
        long startTime = System.currentTimeMillis();  // 记录开始时间
        SXSSFWorkbook wb = null;

        try {
            int rowAccessWindowSize = 100;  // 设置适当的行访问窗口大小
            wb = new SXSSFWorkbook(rowAccessWindowSize);
            wb.setCompressTempFiles(true);  // 启用临时文件压缩以提高性能

            Sheet sheet = wb.createSheet(sheetName);  // 注意这里创建 Sheet 时使用的是 SXSSFWorkbook
            int rowNum = 0;
            Row row = sheet.createRow(rowNum);
            Map<String, CellStyle> cellStyles = initStyles(wb);
            int cellNum = 0;

            // 写入表头
            for (Map.Entry<String, String> entry : header.entrySet()) {
                String fieldDesc = entry.getValue();
                Cell cell = row.createCell(cellNum);
                cell.setCellValue(fieldDesc);
                logger.info("导入数据到excel==========> 表头" + entry.getKey());
                cellNum++;
            }

            int totalRecords = ObjectKit.isNotEmpty(datas) ? datas.size() : 0;  // 总共导出记录数
            logger.info("总共导出记录数: " + totalRecords);

            if (ObjectKit.isNotEmpty(datas)) {
                for (Map<String, Object> map : datas) {
                    rowNum++;
                    row = sheet.createRow(rowNum);
                    logger.info("导入数据到excel==========> 行====" + JSON.toJSONString(map));
                    cellNum = 0;

                    for (Map.Entry<String, String> entry : header.entrySet()) {
                        String fieldName = entry.getKey();
                        Object data = map.get(fieldName.toUpperCase());
                        String dataString = null == data ? "" : data.toString();

                        if (data instanceof BigDecimal) {
                            Cell cell = row.createCell(cellNum);
                            cell.setCellValue(((BigDecimal) data).toPlainString());
                            cell.setCellStyle(cellStyles.get("Number"));
                        } else {
                            if (data instanceof Date || data instanceof Timestamp) {
                                if (data.toString().contains(".")) {
                                    dataString = null == data ? "" : data.toString().substring(0, data.toString().indexOf("."));
                                } else {
                                    dataString = null == data ? "" : data.toString();
                                }
                            }
                            row.createCell(cellNum).setCellValue(null == data ? "" : dataString);
                        }
                        cellNum++;
                    }

                    // 当前已导出记录数及进度
                    logger.info("当前已导出记录数: " + rowNum + ", 进度: " + ((float) rowNum / totalRecords) * 100 + "%");
                }
            }
            logger.info("导入数据到excel==========> 结束");
            long endTime = System.currentTimeMillis();  // 记录结束时间
            long elapsedTime = endTime - startTime;  // 计算耗时时间
            logger.info("总共导出记录数: " + totalRecords);
            logger.info("耗时时间: " + elapsedTime + " 毫秒");

            wb.write(os);
        } catch (Exception e) {
            throw new ImpException(ImpError.APP_ERR_20_04_10, e);
        } finally {
            try {
                if (null != wb) {
                    wb.close();
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

如果还想进一步优化:可以考虑样式重用

修改前:

  /**
     * Excel样式初始化
     * @param wb
     * @return
     */
    private Map<String,CellStyle> initStyles(SXSSFWorkbook wb){
        Map<String,CellStyle> styles = new HashMap<String,CellStyle>();
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        styles.put("String",style);
        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.RIGHT);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setDataFormat(wb.createDataFormat().getFormat("#,##0.00"));
        styles.put("Number",style);
        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.RIGHT);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setDataFormat(wb.createDataFormat().getFormat("0.00%"));
        styles.put("Percent",style);
        return styles;
    }

修改后:通过在方法内部检查样式对象是否已经存在来避免重复创建。使用一个 Map 或者类似的数据结构来存储已创建的样式对象,以实现样式对象的复用

private Map<String, CellStyle> styleCache = new HashMap<>();

private Map<String, CellStyle> initStyles(SXSSFWorkbook wb) {
    Map<String, CellStyle> styles = new HashMap<>();

    // 初始化 String 样式
    CellStyle stringStyle = getOrCreateStyle(wb, STYLE_STRING, HorizontalAlignment.CENTER, VerticalAlignment.CENTER, null);
    styles.put(STYLE_STRING, stringStyle);

    // 初始化 Number 样式
    CellStyle numberStyle = getOrCreateStyle(wb, STYLE_NUMBER, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER, "#,##0.00");
    styles.put(STYLE_NUMBER, numberStyle);

    // 初始化 Percent 样式
    CellStyle percentStyle = getOrCreateStyle(wb, STYLE_PERCENT, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER, "0.00%");
    styles.put(STYLE_PERCENT, percentStyle);

    return styles;
}

private CellStyle getOrCreateStyle(SXSSFWorkbook wb, String key, HorizontalAlignment alignment, VerticalAlignment verticalAlignment, String dataFormat) {
    if (styleCache.containsKey(key)) {
        return styleCache.get(key);
    } else {
        CellStyle style = createStyle(wb, alignment, verticalAlignment, dataFormat);
        styleCache.put(key, style);
        return style;
    }
}

private CellStyle createStyle(SXSSFWorkbook wb, HorizontalAlignment alignment, VerticalAlignment verticalAlignment, String dataFormat) {
    CellStyle style = wb.createCellStyle();
    style.setAlignment(alignment);
    style.setVerticalAlignment(verticalAlignment);
    if (dataFormat != null) {
        style.setDataFormat(wb.createDataFormat().getFormat(dataFormat));
    }
    return style;
}

验证导出百万+数据:正常

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值