Java应用使用H2数据库后内存占用未释放的原因及解决方法
H2数据库操作后内存未释放问题分析
测试场景与现象
- 应用启动后前15秒内存占用11MB
- 创建数据表并循环写入数据时,内存占用上升
- 循环执行完毕后已无数据库连接,但内存仍被占用
- 增加循环次数,内存占用不再增长或仅小幅上升
疑问
- 为何会出现这种情况?内存不应被释放吗?
- 如何实现内存释放?
- 这部分占用的内存是什么?
已尝试的操作
- 修改创建表的字段
- 强制关闭数据库连接,期望DBMS在访问结束后不再占用内存
内存变化截图


测试代码
import java.io.File; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Date; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import java.sql.DriverManager; import java.sql.PreparedStatement; public class H2CreateExample { private static String jdbcURL = "jdbc:h2:~/biba"; private static String jdbcUsername = "sa"; private static String jdbcPassword = ""; private static Connection getConnection() { Connection connection = null; try { connection = DriverManager.getConnection(jdbcURL, jdbcUsername, jdbcPassword); } catch (SQLException e) { e.printStackTrace(); } return connection; } private static void printSQLException(SQLException ex) { for(Throwable e: ex) { if(e instanceof SQLException) { e.printStackTrace(System.err); System.err.println("SQLState: " + ((SQLException) e).getSQLState()); System.err.println("Error Code: " + ((SQLException) e).getErrorCode()); System.err.println("Message: " + e.getMessage()); Throwable t = ex.getCause(); while (t != null) { System.out.println("Cause: " + t); t = t.getCause(); } } } } private static final String createTableSQL = "CREATE TABLE `data` (\n" + " `id` BIGINT auto_increment,\n" + " `date` DATE NOT NULL,\n" + " `dataText` TEXT,\n" + " `dataImage` JAVA_OBJECT,\n" + " PRIMARY KEY (`id`)" + ");"; private static void createDataTable() throws SQLException { try (Connection connection = getConnection(); Statement statement = connection.createStatement()) { statement.execute(createTableSQL); } catch (SQLException e) { printSQLException(e); } } private static final String INSERT_USERS_SQL = "INSERT INTO `data`" + " (`date`, `dataText`, `dataImage`) VALUES " + " (?, ?, ?);"; public static long add(Date date, String dataText, ImageIcon image) throws SQLException { long result = -1; try (Connection connection = getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(INSERT_USERS_SQL)) { preparedStatement.setDate(1, new java.sql.Date(date.getTime())); preparedStatement.setString(2, dataText); preparedStatement.setObject(3, image); preparedStatement.executeUpdate(); } catch (SQLException e) { printSQLException(e); } return result; } public static void main(String[] argv) throws SQLException { try { Thread.sleep(15000); } catch (InterruptedException e) { } createDataTable(); System.out.println("Таблица создана"); File file = new File("/home/drevnicky/Images/black.jpg"); ImageIcon image = null; try { image = new ImageIcon(ImageIO.read(file)); } catch (IOException e) { } for(int k = 0; k<50; k++) { add(new Date(), "Text 123", image); System.out.println("k " + k); } try { Thread.sleep(15000); } catch (InterruptedException e) { } } }
问题解答
1. 内存未释放的原因
- H2的缓存与连接复用机制:H2默认会维护连接池,即使显式关闭连接,驱动也可能缓存连接用于后续复用;同时H2会缓存表结构、查询计划、数据页等内容,这些缓存会持续占用内存。
- Java垃圾回收特性:Java的GC不会在对象变为垃圾后立即回收,需等待触发条件(如内存阈值)。操作产生的临时对象(如PreparedStatement实例、序列化的ImageIcon副本)可能仍留在堆中未被清理。
- 序列化对象的内存留存:存储
ImageIcon(JAVA_OBJECT类型)时,H2处理序列化过程中会产生临时缓存,JVM也可能留存序列化/反序列化的字节数组等数据。
2. 实现内存释放的方法
- 显式关闭H2数据库:操作完成后执行
SHUTDOWN命令,强制释放H2的缓存和资源:try (Connection conn = getConnection()) { Statement stmt = conn.createStatement(); stmt.execute("SHUTDOWN"); } catch (SQLException e) { printSQLException(e); } - 触发垃圾回收:调用
System.gc()(仅为JVM提供GC建议,无法强制),生产环境不推荐依赖此方法。 - 调整H2配置:修改JDBC URL禁用不必要的缓存和连接池,例如:
jdbc:h2:~/biba;DB_CLOSE_ON_EXIT=FALSE;CACHE_SIZE=0(根据实际需求调整参数)。
3. 占用内存的构成
- H2内部缓存:表元数据、数据页缓存、查询计划缓存等。
- JVM堆临时对象:数据库连接实例、PreparedStatement、序列化的ImageIcon字节数组、SQL执行过程中的临时数据结构。
- 驱动相关资源:H2 JDBC驱动加载后占用的内存,以及其内部维护的全局状态。
4. 增加循环次数内存不再增长的原因
循环次数增加时,H2的缓存会达到上限,后续操作复用已有缓存(如PreparedStatement缓存、数据页缓存);同时JVM会复用已分配的堆内存(对象回收后,新对象直接占用空闲内存空间),因此内存占用不再大幅上升。
内容的提问来源于stack exchange,提问作者drevnicky
相关产品推荐
相关产品推荐

