You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java应用使用H2数据库后内存占用未释放的原因及解决方法

H2数据库操作后内存未释放问题分析

测试场景与现象

  • 应用启动后前15秒内存占用11MB
  • 创建数据表并循环写入数据时,内存占用上升
  • 循环执行完毕后已无数据库连接,但内存仍被占用
  • 增加循环次数,内存占用不再增长或仅小幅上升

疑问

  1. 为何会出现这种情况?内存不应被释放吗?
  2. 如何实现内存释放?
  3. 这部分占用的内存是什么?

已尝试的操作

  • 修改创建表的字段
  • 强制关闭数据库连接,期望DBMS在访问结束后不再占用内存

内存变化截图

内存变化截图1
内存变化截图2

测试代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:15:33