Java 数据源内存泄漏
Java memory leak with a data source
我多次听说不关闭数据库连接可能会导致内存泄漏。
(比如在这个article)
我尝试通过从 org.apache.commons.dbcp2.BasicDataSource[=31 获取连接来重现同样的问题=]
而不是关闭它。
这是我的代码:
//Repo class
private final BasicDataSource ds;
public Repo() {
ds = new BasicDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl("jdbc:postgresql://localhost/postgres");
ds.setUsername("postgres");
ds.setPassword("postgres");
ds.setMaxOpenPreparedStatements(10000);
ds.setMaxTotal(10000);
}
public PreparedStatement prepStatement(String sql) throws SQLException {
return this.ds.getConnection().prepareStatement(sql);
}
//Logic class
public JsonNode logic(String name) {
PreparedStatement ps = this.repo.prepStatement("select data from public.users where name = ? ");
ps.setString(1, name);
//The rest of the logic - no close method or try with resource
}
我已经将相同的过程重复了将近 400 次,并且还减小了初始和最大堆大小。
尽管如此,仍然没有内存泄漏的迹象。即使在 VisualVM 监控中,堆图看起来也很正常:
关于如何重现这个问题有什么想法吗?
不关闭资源的问题不是潜在的内存泄漏,而是潜在的资源泄漏。我们谈论的是文件句柄、网络连接,甚至是可能在数据库服务器端分配的资源,在您的 JVM 中根本不可见。
这取决于特定的数据库驱动程序,实际的 PreparedStatement
实现是否具有像终结器或清理器这样的保护措施,当对象被垃圾收集时将关闭资源。但即使它有,这也意味着资源将被保留,直到随后的垃圾收集周期识别出无法访问的对象并触发终结。
在您的特定设置中,似乎每分钟都有一次垃圾回收。也许,关键的 non-memory 资源在那些时候得到了清理;你甚至都没有检查它们。
但是,即使这些资源已为此设置清理干净,您也必须注意以下几点:
并非每个数据库驱动程序都可以这样工作
在实际生产环境中,持有关键 non-memory 资源(如锁、文件句柄、数据库连接等)的时间超过必要时间一分钟,可能已经是一个大问题。
不能保证您每分钟都有一次垃圾回收。系统可能 运行 数小时甚至数天没有进行垃圾回收。
不保证垃圾回收会识别特定的无法访问的对象。这在下一次收集发生时对象属于年轻一代的简单设置中可能会顺利运行,但现代并发收集器乐于在短的、可配置的时间限制内回收大量内存,而不急于收集每个对象.
就内存而言,每个对象的内存都是平等的,因此,这种“物超所值”的集合忽略了哪些对象是无关紧要的。因此 PreparedStatement
实例可能是每个集合都忽略的不幸的无法访问的对象之一。几个字节无关紧要,它会阻塞,这就是允许此策略的原因。如前所述,问题是它可能无限期持有的 non-memory 资源。
我多次听说不关闭数据库连接可能会导致内存泄漏。
(比如在这个article)
我尝试通过从 org.apache.commons.dbcp2.BasicDataSource[=31 获取连接来重现同样的问题=] 而不是关闭它。
这是我的代码:
//Repo class
private final BasicDataSource ds;
public Repo() {
ds = new BasicDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl("jdbc:postgresql://localhost/postgres");
ds.setUsername("postgres");
ds.setPassword("postgres");
ds.setMaxOpenPreparedStatements(10000);
ds.setMaxTotal(10000);
}
public PreparedStatement prepStatement(String sql) throws SQLException {
return this.ds.getConnection().prepareStatement(sql);
}
//Logic class
public JsonNode logic(String name) {
PreparedStatement ps = this.repo.prepStatement("select data from public.users where name = ? ");
ps.setString(1, name);
//The rest of the logic - no close method or try with resource
}
我已经将相同的过程重复了将近 400 次,并且还减小了初始和最大堆大小。
尽管如此,仍然没有内存泄漏的迹象。即使在 VisualVM 监控中,堆图看起来也很正常:
关于如何重现这个问题有什么想法吗?
不关闭资源的问题不是潜在的内存泄漏,而是潜在的资源泄漏。我们谈论的是文件句柄、网络连接,甚至是可能在数据库服务器端分配的资源,在您的 JVM 中根本不可见。
这取决于特定的数据库驱动程序,实际的 PreparedStatement
实现是否具有像终结器或清理器这样的保护措施,当对象被垃圾收集时将关闭资源。但即使它有,这也意味着资源将被保留,直到随后的垃圾收集周期识别出无法访问的对象并触发终结。
在您的特定设置中,似乎每分钟都有一次垃圾回收。也许,关键的 non-memory 资源在那些时候得到了清理;你甚至都没有检查它们。
但是,即使这些资源已为此设置清理干净,您也必须注意以下几点:
并非每个数据库驱动程序都可以这样工作
在实际生产环境中,持有关键 non-memory 资源(如锁、文件句柄、数据库连接等)的时间超过必要时间一分钟,可能已经是一个大问题。
不能保证您每分钟都有一次垃圾回收。系统可能 运行 数小时甚至数天没有进行垃圾回收。
不保证垃圾回收会识别特定的无法访问的对象。这在下一次收集发生时对象属于年轻一代的简单设置中可能会顺利运行,但现代并发收集器乐于在短的、可配置的时间限制内回收大量内存,而不急于收集每个对象.
就内存而言,每个对象的内存都是平等的,因此,这种“物超所值”的集合忽略了哪些对象是无关紧要的。因此
PreparedStatement
实例可能是每个集合都忽略的不幸的无法访问的对象之一。几个字节无关紧要,它会阻塞,这就是允许此策略的原因。如前所述,问题是它可能无限期持有的 non-memory 资源。