Java垃圾回收机制判定对象可销毁的方式
- 引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。当计数器值为0时,就意味着这个对象没有任何地方被引用,该对象就可以被销毁。但是Java虚拟机并没有采用这种方式,因为它很难解决对象之间相互循环引用的问题。例如,对象A持有对象B的引用,对象B也持有对象A的引用,即使这两个对象在外部都不再被使用,但它们的引用计数都不会为0,导致无法被回收。
- 可达性分析算法:Java虚拟机采用该算法来判定对象是否可被销毁。该算法以一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以被销毁。在Java中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:比如方法中的局部变量所指向的对象。
- 方法区中类静态属性引用的对象:例如类的静态成员变量指向的对象。
- 方法区中常量引用的对象:如字符串常量池中的字符串对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
实际开发中可能导致内存泄漏的情况及避免方法
- 静态集合类导致的内存泄漏
- 情况描述:当静态集合类(如
HashMap
、ArrayList
等)被定义为静态成员变量时,如果向其中添加对象后没有及时清理,即使这些对象在程序其他地方不再使用,但由于静态集合类的生命周期与应用程序相同,这些对象不会被垃圾回收,从而导致内存泄漏。例如,在一个工具类中定义了一个静态HashMap
,并在不同地方向其中添加对象,但没有提供相应的清理方法,随着程序运行,HashMap
中积累的对象越来越多,占用内存也越来越大。
- 避免方法:尽量避免使用静态集合类来长时间保存对象。如果确实需要使用,提供清理方法,在对象不再需要时及时从集合中移除。例如,为上述工具类添加一个
clear()
方法,在合适的时机调用该方法清理HashMap
中的对象。
- 监听器和回调未注销导致的内存泄漏
- 情况描述:在使用监听器模式或回调机制时,如果注册了监听器或回调,但在对象不再使用时没有注销,被监听对象或回调对象会持有对注册对象的引用,导致注册对象无法被垃圾回收。比如,在一个图形界面应用中,一个组件注册了一个监听器,但在组件销毁时没有注销监听器,监听器会一直持有对组件的引用,使得组件及其相关资源无法被回收。
- 避免方法:在对象的生命周期结束时,确保及时注销监听器或回调。例如,在组件的销毁方法中添加注销监听器的代码。
- 数据库连接、文件句柄等资源未关闭导致的内存泄漏
- 情况描述:在使用数据库连接、文件句柄等资源时,如果在使用完毕后没有正确关闭,这些资源会一直占用系统资源,并且相关对象可能不会被垃圾回收,造成内存泄漏。例如,在进行数据库操作时,获取了数据库连接
Connection
对象,但在操作完成后没有调用connection.close()
方法关闭连接,连接对象会一直占用内存和数据库资源。
- 避免方法:使用
try - finally
块或Java 7引入的try - with - resources
语句来确保资源被正确关闭。例如,对于数据库连接:
try (Connection connection = DriverManager.getConnection(url, username, password)) {
// 数据库操作
} catch (SQLException e) {
e.printStackTrace();
}
- 内部类持有外部类引用导致的内存泄漏
- 情况描述:非静态内部类会隐式持有外部类的引用,如果内部类对象的生命周期比外部类对象长,外部类对象在应该被销毁时,由于内部类的引用而无法被垃圾回收,从而导致内存泄漏。比如,在一个Activity中定义了一个非静态内部类作为线程类,当Activity销毁时,如果线程还在运行,线程(内部类对象)会持有Activity(外部类对象)的引用,使得Activity及其相关资源无法被回收。
- 避免方法:将内部类定义为静态内部类,如果内部类需要访问外部类的成员,可以通过弱引用的方式来持有外部类对象。例如:
public class OuterClass {
private static class InnerClass {
private WeakReference<OuterClass> outerWeakRef;
public InnerClass(OuterClass outer) {
outerWeakRef = new WeakReference<>(outer);
}
// 内部类方法
}
}
- 缓存导致的内存泄漏
- 情况描述:当使用缓存来存储对象时,如果缓存没有设置合理的大小限制或过期策略,随着对象不断加入缓存,缓存占用的内存会越来越大,并且即使对象在程序其他地方不再使用,由于缓存的持有,这些对象也不会被垃圾回收,导致内存泄漏。例如,在一个应用中使用
ConcurrentHashMap
作为缓存,不断向其中添加对象,但没有限制缓存的大小,随着时间推移,缓存占用内存可能会耗尽系统内存。
- 避免方法:为缓存设置合理的大小限制,当缓存达到上限时,采用合适的淘汰策略(如LRU - 最近最少使用)移除对象。或者为缓存中的对象设置过期时间,定期清理过期对象。例如,可以使用
Guava Cache
,它提供了设置缓存大小、过期时间等功能。
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
// 加载缓存数据的逻辑
return null;
}
});