从Access数据库导入25万行至JTable遇Table model溢出及内存错误求助
解决JTable导入25万行Access数据时的OutOfMemoryError问题
这个问题我帮不少开发者处理过——一次性把25万行数据全塞进内存里的TableModel,JVM肯定扛不住,GC overhead limit exceeded错误就是JVM在疯狂垃圾回收却挤不出可用空间的典型信号。下面给你几个从治标到治本的解决方案:
1. 虚拟列表(按需加载)——治本之策
默认的DefaultTableModel会把所有数据存在内存里,这对大数据集完全不友好。你可以自己实现虚拟TableModel,只加载当前JTable可见区域的行数据,从数据库按需查询:
public class VirtualTableModel extends AbstractTableModel { private int totalRows; // 先通过COUNT(*)获取总记录数 private List<Object[]> currentPageData; private final int pageSize = 1000; // 每页加载1000行,可调整 public VirtualTableModel() { // 初始化时查询总记录数 try (Connection conn = getAccessConnection()) { Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM your_table"); if (rs.next()) { totalRows = rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } } @Override public int getRowCount() { return totalRows; } @Override public int getColumnCount() { // 根据你的实际列数返回,也可从ResultSetMetaData动态获取 return 6; } @Override public Object getValueAt(int rowIndex, int columnIndex) { // 计算当前行所属页码,按需加载数据 int pageNum = rowIndex / pageSize; int rowInPage = rowIndex % pageSize; if (currentPageData == null || pageNum != getCurrentPage()) { loadPage(pageNum); } return currentPageData.get(rowInPage)[columnIndex]; } private void loadPage(int pageNum) { currentPageData = new ArrayList<>(); int startRow = pageNum * pageSize; // Access的分页查询写法(需根据你的主键字段调整) String sql = String.format( "SELECT TOP %d * FROM your_table WHERE ID NOT IN (SELECT TOP %d ID FROM your_table)", pageSize, startRow ); try (Connection conn = getAccessConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { ResultSetMetaData meta = rs.getMetaData(); int colCount = meta.getColumnCount(); while (rs.next()) { Object[] row = new Object[colCount]; for (int i = 0; i < colCount; i++) { row[i] = rs.getObject(i+1); } currentPageData.add(row); } } catch (SQLException e) { e.printStackTrace(); } } // 实现你的Access数据库连接逻辑 private Connection getAccessConnection() throws SQLException { String url = "jdbc:ucanaccess://path/to/your/database.accdb"; return DriverManager.getConnection(url); } private int getCurrentPage() { return currentPageData == null ? -1 : (totalRows / pageSize); } }
把这个Model绑定到JTable后,只会加载当前可见区域的数据,内存占用会大幅降低,再也不会出现内存溢出。
2. 临时调大JVM内存——应急方案
如果不想马上改代码,可以先调整JVM启动参数扩容堆内存:
- 添加
-Xmx4g:把最大堆内存设为4GB(根据你的机器内存调整,比如8G内存可以设-Xmx6g) - 可选添加
-XX:-UseGCOverheadLimit:关闭GC overhead的检查(但这只是掩盖问题,数据量再大还是会OOM)
设置方式:IDE运行时在Run Configuration的VM options里添加;jar包运行时用java -Xmx4g -jar yourApp.jar启动。
3. 后台线程分批加载——兼顾UI响应
不要在AWT事件线程(报错里的AWT-EventQueue-0)里加载所有数据,用SwingWorker在后台分批读取,避免UI卡死,同时逐步加载数据:
new SwingWorker<Void, List<Object[]>>() { @Override protected Void doInBackground() throws Exception { int batchSize = 2000; int offset = 0; while (true) { String sql = String.format( "SELECT TOP %d * FROM your_table WHERE ID NOT IN (SELECT TOP %d ID FROM your_table)", batchSize, offset ); List<Object[]> batchData = new ArrayList<>(); try (Connection conn = getAccessConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { ResultSetMetaData meta = rs.getMetaData(); int colCount = meta.getColumnCount(); if (!rs.next()) break; // 没有更多数据 do { Object[] row = new Object[colCount]; for (int i = 0; i < colCount; i++) { row[i] = rs.getObject(i+1); } batchData.add(row); } while (rs.next()); } publish(batchData); // 将分批数据传给UI线程更新 offset += batchSize; } return null; } @Override protected void process(List<List<Object[]>> chunks) { // 在UI线程安全更新TableModel DefaultTableModel model = (DefaultTableModel) yourJTable.getModel(); for (List<Object[]> batch : chunks) { for (Object[] row : batch) { model.addRow(row); } } } }.execute();
这个方案不会一次性占满内存,而且UI不会卡住,但内存仍会随加载数据增多而增长,适合数据量不是极端大的场景。
4. 其他优化细节
- 关闭数据库资源:用try-with-resources自动关闭ResultSet、Statement、Connection,避免内存泄漏
- 用轻量级数据结构:用
Object[]存储行数据比ArrayList<Object>更省内存;固定结构数据用自定义VO类代替通用集合 - 精简查询列:不要用
SELECT *,只查询需要的列,减少每行数据量
内容的提问来源于stack exchange,提问作者Ajay
相关产品推荐
相关产品推荐

