excel文件写性能优化

一 问题背景

  通过POI框架生成excel文件的时候,一般选用的是XSSFWorkbook类。用这个类生成excel文件时,文件的所有数据均会存放在内存当中,包括cell对象,cellType对象等等。如果数据少的话,内存还能撑得住,但是超过了万行级别,内存就会被打爆。POI框架开发者直到3.8版本才提供了SXSSFWorkbook类用以解决这个问题。

Since 3.8-beta3, POI provides a low-memory footprint SXSSF API built on top of XSSF.

  相比XSSFWorkbook把所有的行数据存放在内存,SXSSF通过限制对滑动窗口中的行的访问来实现其低内存占用。当新创建的行的数量超过滑动窗口的上限时,滑动窗口里的数据将会被写入磁盘,同时清空滑动窗口的行。这也意味着,写入磁盘的行将不能再发生改变。

SXSSF is an API-compatible streaming extension of XSSF to be used when very large spreadsheets have to be produced, and heap space is limited. SXSSF achieves its low memory footprint by limiting access to the rows that are within a sliding window, while XSSF gives access to all rows in the document.

二 性能测试

做一个性能测试来对比这两种方式的内存消耗。

性能测试工具:Jprofiler 9.2
测试数据:写一个5W行, 每行10列的excel文件
测试机器:MacBook Pro, i7 标压4系, 16G内存

1. 用XSSFWorkbook生成excel文件

a. 测试代码

/**
 * @auther 
 * @date 2018/2/9
 **/
public class XssfWorkBookTest {

    public static final int ROW_NUM = 50000;
    public static final int COL_NUM = 10;

    public static void main(String[] args) {
        Stopwatch    stopwatch    = Stopwatch.createStarted();
        XSSFWorkbook xssfWorkbook = new XSSFWorkbook();
        XSSFSheet    xssfSheet    = xssfWorkbook.createSheet("XSSFWorkbook");
        for (int rowIndex = 0; rowIndex < ROW_NUM; rowIndex++) {
            XSSFRow row = xssfSheet.createRow(rowIndex);
            for (int colIdx = 0; colIdx < COL_NUM; colIdx++) {
                XSSFCell cell = row.createCell(colIdx);
                cell.setCellValue(Joiner.on("_").join("XSSFWorkbook", rowIndex, colIdx));
            }
        }

        try {
            stopwatch.stop();
            System.out.println("--------finish, " + stopwatch.toString());
            Thread.sleep(100000 * 1000);
            xssfWorkbook.write(new FileOutputStream("/temp/XssfWorkBookTest.xlsx"));
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

b. excel文件生成耗时

程序输出

--------finish, 9.570 s

c. Jprofiler监控

  1. 内存概览
    Xssf.png
  2. 大对象统计直方图
    xssfBigClass.png

2. 用流式API的SXSSFWorkbook生成excel文件

a. 测试代码

/**
 * @auther 
 * @date 2018/2/9
 **/
public class SXssfWorkBookTest {

    public static final int ROW_NUM = 50000;
    public static final int COL_NUM = 10;

    public static void main(String[] args) {
        Stopwatch     stopwatch     = Stopwatch.createStarted();
        SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(100);
        SXSSFSheet    sxssfSheet    = sxssfWorkbook.createSheet("SXSSFWorkbook");
        for (int rowIndex = 0; rowIndex < ROW_NUM; rowIndex++) {
            SXSSFRow row = sxssfSheet.createRow(rowIndex);
            for (int colIdx = 0; colIdx < COL_NUM; colIdx++) {
                SXSSFCell cell = row.createCell(colIdx);
                cell.setCellValue(Joiner.on("_").join("SXSSFWorkbook", rowIndex, colIdx));
            }
        }

        try {
            stopwatch.stop();
            System.out.println("--------finish, " + stopwatch.toString());
            Thread.sleep(100000 * 1000);
            sxssfWorkbook.write(new FileOutputStream("/temp/SXssfWorkBookTest.xlsx"));
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

b. excel文件生成耗时

程序输出

--------finish, 3.984 s

c. Jprofiler监控

  1. 内存概览
    sxssf

  2. 大对象统计直方图
    sxssfBigClass

三 测试结果对比分析

1. 耗时对比

  很意外,有频繁磁盘IO的流式API居然会比纯内存操作的API更快, 快了5.586s。我的滑动窗口是100行,总共写入5W行,也就是需要500次磁盘IO。Why? XSSFWorkbook的对象需要常驻内存,50W个cell对象不断生成时,eden区不断的打满,然后触发Yong GC。晋升到老年代的对象需要heap管理器进行内存分配,老年代堆内存分配也需要消耗时间。然而,SXSSFWorkbook的滑动窗口最多只维护1000个cell对象,不会产生大对象,不需要heap管理器分配老年代的内存。所以,流式API反而更快。查下GC信息:

XSSFWorkbook示例gc数据

➜  /work jstat -gcutil 14349
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  53.56  24.42  65.91  96.51  88.33     15    0.831     3    2.299    3.130

流式api GC信息

➜  /work jstat -gcutil 14439
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  45.06  75.26   0.03  96.07  96.14      5    0.027     0    0.000    0.027

  使用XSSFWorkbook的示例发生了3次FullGC,GC总耗时比流式API多了3.103s。这中间还差的2.483s耗在哪里了呢?个人推测是耗在了老年代的堆内存分配上了。堆内存分配是要走空闲内存链表分配算法的,内存操作虽然快,但是这么多的对象需要被分配,时间就累积出来了(不积跬步无以至千里。。。。)。

JVM

2. 进程内存对比

   流式API在一次GC之后内存直接降到了10MB,而XSSFWorkbook的内存一直维持在1.19GB,内存耗用比例是 1:119

3. 大对象对比

  由于XSSFWorkbook会把所有的子对象维护在内存,一个XssfWorkSheet对象占用了371MB,一个SharedStringsTable耗用了226MB内存。由于流式API会不断的把内存数据写入磁盘,所以他的heap几乎没有大对象。

四、流式API使用注意点

能量是守恒的,凡事有利必有弊。流式API虽然不吃内存,但在使用时,需要注意一些问题点。

  • 临时磁盘文件会十分的大,一个20M的excel文件产生的临时文件可能会大于1G。

SXSSF flushes sheet data in temporary files (a temp file per sheet) and the size of these temporary files can grow to a very large value. For example, for a 20 MB csv data the size of the temp xml becomes more than a gigabyte.

解决办法是,开启压缩配置项

SXSSFWorkbook wb = new SXSSFWorkbook(); 
wb.setCompressTempFiles(true); // temp files will be gzipped
  • 临时文件需要手动显式删除,调用dispose方法 即可

猜你喜欢

转载自blog.csdn.net/bruce128/article/details/79351462