Java内存泄漏的8种情况介绍
引言
Java 的 JVM 引入了垃圾回收机制 GC,会将不再使用的对象回收,GC 利用引用计数法和根搜索算法来进行可达性检测,来识别对象是否是不再使用的对象,其本质都是判断一个对象是否还在被引用,那么在这种情况下,由于代码编写的差异化,可能出现内存泄漏问题。
什么是内存泄漏
内存泄漏是指某一段内存空间在使用完毕后未被回收,长期占用,在不涉及复杂数据结构算法的一般情况下,其表现为一个内存对象生命周期超出了程序需要或使用它的时间长度,也将其成为 “对象游离”。
内存泄漏的8种情况
1、静态集合类
如 ArrayList、HashMap 等等集合类在类中创建为静态变量时,那么他们的生命周期与程序是一致的,由于一些集合没有删除元素的特性或者并没有对其包含的对象进行处理,导致容器中对象的生命周期与容器一致,不能被释放回收,然而对象本已经不再使用,就造成了内存泄漏。这样看来,基本特征为长生命周期对象持有短生命周期对象的引用,尽管短生命周期对象已不再使用,但是因为长生命周期对象的引用使其不能被GC回收。
2、各种连接使用后未关闭
如数据库连接、网络 http 连接、io 连接等等。当程序操作数据库时,首先应该建立数据库的连接 Connection,操作语句时建立 Statement 对象,获取结果集建立 ResultSet 对象,之后需要显示的调用 close 方法来关闭连接,只有关闭连接后,GC 才会对对应的未使用对象进行回收。没有及时的关闭数据库连接,会导致大量对象长期占用内存空间,导致内存泄漏。
3、变量的作用域不合理
如本该唯一定义在某方法的变量定义在了全局变量。一般来讲,一个变量的作用范围大于其所被使用的范围,可能发生内存泄漏,表现在存在时间大于使用时间,即使用完了但是还不能被回收,就比如下面的列子。另外,如果没有及时的将未使用的对象置 null,也有可能导致内存泄漏。
package com.liqia.common.core;
/**
* 内存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
/**
* 消息
*/
private String info;
public void receiveAndSaveInfo() {
// 模拟接受消息
receiveInfo();
// 模拟存储消息
saveInfo();
}
}
这里的变量 info 在方法 receiveAndSaveInfo 中进行赋值和保存,在该方法执行完毕后本应该被 GC 回收,但由于全局变量的生命周期是跟随对象的,所有当方法执行完不能被回收,可能造成内存泄漏。
4、内部类持有外部类
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
5、改变哈希值
当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。
6、栈引起的内存泄漏
这段模拟栈操作的代码存在隐蔽的内存泄漏问题。定位到pop()函数,在return语句中,当我们弹出一个元素时,只是简单的让栈顶指针(size)-1。逻辑上,栈中的这个元素已经弹出,已经没有用了。但是事实上,被弹出的元素依然存在于elements数组中,它依然被elements数组所引用,GC是无法回收被引用着的对象的。也许你期望等这整个栈失去引用(将被GC回收时),栈内的elements数组一起被GC回收。但是实际的使用过程中,又有谁能够预料到这个栈会存活多长时间。为了保险起见,我们需要在弹出一个元素的时候,就让这个元素失去引用,便于GC回收。我们只需要让Pop()函数弹出时,同时解除对弹出元素的引用即可。
package com.liqia.common.core;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
* 内存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public DemoMemoryLeak() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
// Object o = elements[--size];
// elements[size] = null;
// return o;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
7、缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
8、监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保存成为WeakHashMap中的键。
杜绝内存泄漏的方法
-
尽量减少使用静态变量,或者使用完及时赋值为 null
-
明确内存对象的有效作用域,尽量缩小对象的作用域,能用局部变量处理的不用成员变量,因为局部变量弹栈会自动回收
-
减少长生命周期的对象持有短生命周期的引用
-
使用 StringBuilder 和 StringBuffer 进行字符串连接,Sting 和 StringBuilder 以及 StringBuffer 等都可以代表字符串,其中 String 字符串代表的是不可变的字符串,后两者表示可变的字符串。如果使用多个 String 对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降
-
对于不需要使用的对象手动设置 null 值,不管 GC 何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象
-
各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭