首先说一些什么是内存泄漏?
Java中的内存泄漏主要指的是Java堆内存中的对象在不再被需要时,由于某种原因,垃圾回收器(GC)无法回收这些对象所占用的内存,导致这些内存被持续占用,最终可能导致OutOfMemoryError错误。
内存泄漏的原因可能是哪些呢?
1、静态集合类
静态集合的生命周期和JVM一致,所以静态集合类被引用的对象不能被释放。
public class OutOfMemoryErrorTest {
static List list = new ArrayList();
public void test() {
Object object = new Object();
list.add(object);
}
}
2、单例模式
单例模式在初始化后会以静态变量的方式在JVM的整个生命周期中存在。如果单例对象有外部持有的引用,那么这个对象将不能被GC回收,导致内存泄漏。
3、IO使用后没有释放资源
这里主要指创建的创建的连接不在使用的时候,需要调用close方法关闭连接,只有被连接关闭后,GC才会回收对应的对象(Connection、Statement、Resultset、Session),忘记关闭这些资源都会导致持续占有CPU,无法被GC回收。
public class PrintStreamChatTest {
public static void main(String[] args) {
// 由手册可知:构造方法需要的是Reader类型的引用,但Reader类是个抽象类,实参只能传递子类的对象 字符流
// 由手册可知: System.in代表键盘输入, 而且是InputStream类型的 字节流
BufferedReader br = null;
PrintStream ps = null;
try {
br = new BufferedReader(new InputStreamReader(System.in));
ps = new PrintStream(new FileOutputStream("d:/a.txt", true));
//省略
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源 如果不关闭,会导致OOM
if (null != ps) {
ps.close();
}
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4、不合理的变量作用域
一个变量定义作用域大于其使用范围,很可能会存在内存泄露;或不在使用对象后没有及时将对象置为null,很可能导致内存泄漏的发生。关于变量作用域,它主要影响的是变量的可见性和生命周期,而不是直接导致内存泄漏。但是,如果作用域过大,可能会使得代码难以理解和维护,从而间接增加内存泄漏的风险(比如,因为代码复杂而忘记释放资源)。将不再使用的对象显式置为null在Java中通常不是必要的,因为一旦没有任何引用指向该对象,它就会被视为垃圾回收的候选对象。然而,在某些情况下,显式地将对象置为null可以作为一种代码清晰性的手段,或者在某些特定的上下文中(如缓存管理、资源释放等)可能有助于更早地触发垃圾回收。
public class OutOfMemoryErrorTest {
// 静态变量,作用域是整个类,但可能只在某个方法中使用
private static List<Object> staticList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
// 在循环中向静态列表添加对象
staticList.add(new Object());
// 假设这里有一些逻辑,但实际上我们忘记了在某个点清理这个列表
}
// 此时,staticList包含了大量不再需要的对象,但由于它是静态的,
// 这些对象将一直存在,直到程序结束或显式清理
// 正确的做法应该是在适当的时候清理这个列表,比如:
// staticList.clear();
// 但在这个例子中,我们故意不这么做来模拟潜在的内存泄漏
}
}
5、Hash值发生改变
对象Hash值发生改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hash值和存储容器时的Hash值不同,所以无法找到存入的对象,自然也就无法单独删除,这也会导致内存泄漏。HashMap 和 HashSet 等基于哈希表的集合依赖于对象的哈希码(通过调用对象的 hashCode() 方法获得)来快速定位元素。如果对象的哈希码在对象被添加到集合之后发生了改变,那么这个对象在集合中的位置可能会变得不可预测,从而导致无法正确检索或删除该对象。然而,这通常不会直接导致内存泄漏,因为对象仍然存在于集合中,只是无法按预期方式访问。如果集合本身被持续引用,而其中的对象由于哈希码变化而变得无法访问,那么这些对象将占用不必要的内存,这可以被视为一种“逻辑上的内存浪费”,而不是传统意义上的内存泄漏。
public class HashCodeChangeExample {
public static void main(String[] args) {
Map<MutableObject, String> map = new HashMap<>();
MutableObject obj = new MutableObject(1);
map.put(obj, "Initial Value");
System.out.println("Before changing value: " + map.get(obj)); // 输出 Initial Value
// 修改对象的值,这会改变其hashCode
obj.setValue(2);
// 由于hashCode已经改变,现在无法从map中检索到该对象
System.out.println("After changing value: " + map.get(obj)); // 输出 null
// 注意:这里并没有内存泄漏,因为map仍然持有对obj的引用
// 但是,obj在map中变得无法访问,除非我们能够通过其他方式(如遍历map)找到它
// 如果map不再被引用,那么它和其中的所有对象最终都将被垃圾回收
}
6、ThreadLocal使用不当
ThreadLocal的弱引用导致内存泄漏,使用完ThreadLocal一定要用remove方法来进行清除。
/**
* 连接工具类:从数据源中获取一个连接,并且将获取到的连接与线程进行绑定
* ThreadLocal:线程内部的存储类,可以在指定的线程内存储数据key:threadLocal(当前线程) value:任意类型的值 Connection
*/
@Component
public class ConnectionUtils {
@Autowired
private DataSource dataSource;
private ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
/**
* 获取当前线程上绑定连接:如果获取到的连接为空,那么就需要从数据源中获取连接,并且放到ThreadLocal中(绑定当前线程)
* @return
*/
public Connection getThreadConnection(){
//1、先从ThreadLocal上获取连接
Connection connection = threadLocal.get();
//2、判断当前线程中是否有Connection
if (null==connection){
//3、从数据库中获取一个连接,并且存入到ThreadLocal中
try {
connection = dataSource.getConnection();
threadLocal.set(connection);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return connection;
}
/**
* 解除当前线程的绑定
*/
public void removeThreadConnection(){
threadLocal.remove();
}
}