自己挖的坑自己填--jxl進行Excel下載堆記憶體溢位問題

碼猿手發表於2021-04-01

  今天在進行使用 jxl 進行 Excel 下載時,由於資料量大(4萬多條接近5萬條資料的下載),資料結構過於負責,存在大量大物件(雖然在物件每次用完都設定為null,但還是存在記憶體溢位問題),加上本地電腦記憶體不大(只有8G),導致下載資料時報堆記憶體溢位,下載失敗。

Exception data: java.lang.OutOfMemoryError
at jxl.write.biff.MemoryDataOutput.write(MemoryDataOutput.java:72)
at jxl.write.biff.File.write(File.java:149)
at jxl.write.biff.RowRecord.writeCells(RowRecord.java:324)
at jxl.write.biff.SheetWriter.write(SheetWriter.java:479)
at jxl.write.biff.WritableSheetImpl.write(WritableSheetImpl.java:1431)
at jxl.write.biff.WritableWorkbookImpl.write(WritableWorkbookImpl.java:915)

  下面是案例復現的簡單模擬程式碼:

        <dependency>
            <groupId>net.sourceforge.jexcelapi</groupId >
            <artifactId>jxl</artifactId >
            <version>2.6.12</version >
        </dependency >
public class MyExcel {
    public static void main(String[] args) throws Exception {
        File xlsFile = new File("E:\\jxl.xls");
        // 建立一個工作簿
        WritableWorkbook workbook = Workbook.createWorkbook(xlsFile);
        // 建立一個工作表
        WritableSheet sheet = workbook.createSheet("sheet1", 0);
        Map<Integer, ArrayList<Map<Integer, Person>>> mapListMap = new HashMap<>();
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonServiceImpl bean = context.getBean(PersonServiceImpl.class);
        //獲取資料庫資料
        List<Person> listBean = bean.getList();
        //進行資料處理
        for (int row = 0; row < 45000; row++) {
            ArrayList<Map<Integer, Person>> listMap = new ArrayList<Map<Integer, Person>>();
            for (int col = 0; col < 30; col++) {
                Map<Integer, Person> map = new HashMap<Integer, Person>();
                for (int j = 0; j < listBean.size(); j++) {
                    map.put(j, listBean.get(j));
                }
                listMap.add(map);
                map = null;
            }
            mapListMap.put(row, listMap);
            listMap = null;
        }
        //寫資料
        for (int row = 0; row < 45000; row++) {
            ArrayList<Map<Integer, Person>> listMap = mapListMap.get(row);
            for (int col = 0; col < 30; col++) {
                Map<Integer, Person> map = listMap.get(col);
                for (int j = 0; j < listBean.size(); j++) {
                    Person person = map.get(j);
                    sheet.addCell(new Label(col, row, person.getId()+"" + row + col));
                    sheet.addCell(new Label(col, row, person.getName() + row + col));
                    sheet.addCell(new Label(col, row, person.getAge()+"" + row + col));
                    map = null;
                }
            }
            listMap = null;
        }
        workbook.write();
        workbook.close();
    }

   執行後結果:


  解決辦法:

  (1)最簡單的方法是加大記憶體,本地電腦記憶體過小,當把程式碼部署到公司測試環境(測試環境記憶體是16G)時該問題不再復現;

  (2)採用臨時檔案寫入EXCEL功能,設定臨時檔案的位置,可以有效的避免記憶體溢位(jxl版本必須是2.6.12及其以上,2.6版本沒有采用臨時檔案的功能);

public static void main(String[] args) throws Exception {
        File xlsFile = new File("E:\\jxl.xls");
        // 建立一個工作簿
        WorkbookSettings ws = new WorkbookSettings();
        ws.setUseTemporaryFileDuringWrite(true);
        ws.setTemporaryFileDuringWriteDirectory(new File("E:\\"));//指定一個臨時檔案路徑
        WritableWorkbook workbook = Workbook.createWorkbook(xlsFile,ws);
        // 建立一個工作表
        WritableSheet sheet = workbook.createSheet("sheet1", 0);
        Map<Integer, ArrayList<Map<Integer, Person>>> mapListMap = new HashMap<>();
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
        PersonServiceImpl bean = context.getBean(PersonServiceImpl.class);
        //獲取資料庫資料
        List<Person> listBean = bean.getList();
        //進行資料處理
        for (int row = 0; row < 45000; row++) {
            ArrayList<Map<Integer, Person>> listMap = new ArrayList<Map<Integer, Person>>();
            for (int col = 0; col < 30; col++) {
                Map<Integer, Person> map = new HashMap<Integer, Person>();
                for (int j = 0; j < listBean.size(); j++) {
                    map.put(j, listBean.get(j));
                }
                listMap.add(map);
                map = null;
            }
            mapListMap.put(row, listMap);
            listMap = null;
        }
        //寫資料
        for (int row = 0; row < 45000; row++) {
            ArrayList<Map<Integer, Person>> listMap = mapListMap.get(row);
            for (int col = 0; col < 30; col++) {
                Map<Integer, Person> map = listMap.get(col);
                for (int j = 0; j < listBean.size(); j++) {
                    Person person = map.get(j);
                    sheet.addCell(new Label(col, row, person.getId()+"" + row + col));
                    sheet.addCell(new Label(col, row, person.getName() + row + col));
                    sheet.addCell(new Label(col, row, person.getAge()+"" + row + col));
                }
          map = null; } listMap
= null; } workbook.write(); workbook.close(); }

   當開始執行下載時,會產生一個臨時檔案,jxl 會把資料寫入到這個臨時檔案中,寫入結束後會把這個臨時檔案刪除:

  經過 jconsole.exe 監控可以看出:使用了臨時檔案後多了一個執行緒進行類的解除安裝,使用了臨時檔案的執行緒高峰有15個,類解除安裝了6個,沒有使用臨時檔案時執行緒數一直保持14個到報記憶體溢位結束,0個類解除安裝:

  PS:

  (1)Excel 表一個 sheet 頁可以儲存6萬條,所以一般超過5萬條的,其他的資料就儲存其他sheet中;或採用分成幾個 Excel 表來實現資料下載;

  (2)今天測試下載的 Excel 資料文字有170M(資料有4.8w條),通過MS Office 開啟失敗,為空 Excel 表,但通過 WPS 可以正常開啟。

相關文章