导出数据,数据量太大,一直处于加载中,然后刷新页面,也刷新不出来。
原因:
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;
}
验证导出百万+数据:正常