常见Java内存泄漏场景
- 静态集合类导致的内存泄漏
- 根本原因:静态集合类(如
HashMap
、Vector
等)生命周期与应用程序相同。如果在集合中放入对象后,未及时清理,即使这些对象在业务逻辑中不再使用,由于集合对其存在引用,垃圾回收器无法回收这些对象,从而导致内存泄漏。
- 避免方法:在对象不再使用时,及时从静态集合中移除。例如使用
map.remove(key)
方法移除相应的键值对。
- 监听器和回调未注销导致的内存泄漏
- 根本原因:当注册监听器或回调时,如果在不再需要它们时没有注销,被监听的对象会一直持有对监听器或回调对象的引用,即使监听器或回调对象在其他地方已不再使用,垃圾回收器也不能回收它们,造成内存泄漏。
- 避免方法:在对象不再使用时,调用注销方法,例如
removeListener()
方法。
- 数据库连接未关闭导致的内存泄漏
- 根本原因:数据库连接(
Connection
)对象在使用完毕后,如果没有显式关闭,连接对象会一直占用资源,并且相关的资源(如Statement、ResultSet等)也不会被释放,造成内存泄漏。
- 避免方法:在使用完数据库连接相关对象后,在
finally
块中关闭连接、Statement和ResultSet。例如:
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(url, username, password);
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
// 处理结果集
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
- 内部类持有外部类引用导致的内存泄漏
- 根本原因:非静态内部类会隐式持有外部类的引用,如果内部类对象的生命周期比外部类对象长,外部类对象在应该被回收时,由于内部类的引用而无法被回收,导致内存泄漏。
- 避免方法:将内部类声明为静态内部类,如果需要访问外部类的成员,可以通过弱引用(
WeakReference
)来实现。
利用工具排查和定位内存泄漏问题
- Jconsole
- 排查步骤:
- 启动Jconsole,并连接到目标Java进程。
- 在“内存”标签页中,观察堆内存使用情况的变化趋势。如果堆内存持续增长,且在执行垃圾回收后没有明显下降,可能存在内存泄漏。
- 点击“执行GC”按钮手动触发垃圾回收,进一步确认内存是否能被回收。
- 在“线程”标签页中,查看线程状态,检查是否有长时间运行且不释放资源的线程,某些内存泄漏可能与线程异常有关。
- 使用“MBean”中的
com.sun.management:type=Memory
MBean获取详细的内存信息,如各代堆内存的使用情况等,辅助分析。
- 定位方法:如果怀疑存在内存泄漏,可以使用Jconsole的“堆内存”直方图功能。该功能可以列出堆中对象的数量和大小,通过观察对象数量和大小的变化,找出可能导致内存泄漏的对象类型。然后可以结合代码,分析这些对象是如何产生和引用的,从而定位内存泄漏点。
- VisualVM
- 排查步骤:
- 启动VisualVM,并连接到目标Java进程。
- 在“概述”标签页中,可以查看基本的JVM信息和进程状态。
- 在“监视”标签页中,实时监控CPU、内存、类和线程的使用情况。关注内存使用曲线,如果内存持续上升且垃圾回收后不下降,可能存在内存泄漏。
- 点击“垃圾回收”按钮手动触发垃圾回收,观察内存变化。
- 定位方法:
- 使用“抽样器”标签页中的“内存”抽样功能。点击“开始”进行内存抽样,抽样完成后,会展示堆中对象的分布情况,包括对象的数量、大小和类名。通过分析对象数量和大小的增长趋势,找出可疑的对象类型。
- 对于可疑的对象类型,可以查看其引用树。在VisualVM中右键点击对象类型,选择“显示对象” -> “实例”,然后右键点击实例,选择“显示引用树”。通过引用树可以查看对象之间的引用关系,从而定位到内存泄漏的源头,即哪些对象对这些可疑对象存在不必要的强引用。