java 语言的一个重要的特性就是垃圾收集器的自动收集和回收,而不需要我们手动去管理和释放内存,这也让 java 内存泄漏问题更加难以发现和处理。
如果你的程序抛出了 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,那么通常这就是因为内存泄露引起的。
总的来说,释放对象的原则就是他再也不会被使用,给一个对象赋值为 null 或者其他对象,就会让这个对象原来所指向的空间变得无法访问,也就再也无法被使用从而等待 GC 的回收。 内存泄露指的就是虽然这部分对象的内存已经不会再被使用,但是他们却不会被 jvm 回收。
public class Simple {
private Object object;
public void method() {
object = new Object();
// ...
}
}
以上的代码中,我们在 method 方法中为类成员变量 object 赋值了实例化后的值,但是如果我们仅仅在这个方法中使用到了 object,那将意味着在整个类的生命周期中,object 所占用的空间虽然都不会被再次使用,但却始终无法得以回收,这就造成了内存泄露,如果 object 是一个加入了很多元素的容器,则问题将暴露的更加明显。
上述内存泄露代码的改进比较简单。
public class Simple {
private Object object;
public void method() {
object = new Object();
// 使用到 object 的业务代码
object = null;
}
}
解决内存泄露问题的原则就是在对象不再被使用的时候立即释放相应的引用,因此在业务代码执行后,object 对象不再使用时,赋值为 null,释放他的引用就可以让 jvm 回收相应的内存了。
下面是一段 jdk8 LinkedList 的源码。
//删除指定节点并返回被删除的元素值
E unlink(Node<E> x) {
//获取当前值和前后节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
//如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点
first = next;
} else {
//如果前一个节点不为空,那么他先后指向当前的下一个节点
prev.next = next;
x.prev = null;
}
if (next == null) {
//如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点
last = prev;
} else {
//如果后一个节点不为空,后一个节点向前指向当前的前一个节点
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
可以看到,在对 x 的成员 next、item、prev 的使用结束后,都显式赋值了 null,以免他们无法被 jvm 回收,在实际开发中,很容易被忽略。
下面是我们通过 ArrayList 实现的一个 pop 方法。
public E pop(){
if(size == 0)
return null;
else
return (E) elementData[--size];
}
实现起来非常简单,但是却存在着内存泄露的问题,因为 size 变小导致 ArrayList 中原有的末端元素将永远得不到使用,但是由于容器持有着他们的引用,他们也永远得不到释放。
public E pop(){
if(size == 0)
return null;
else{
E e = (E) elementData[--size];
elementData[size] = null;
return e;
}
}
通过主动赋值为 null 从而释放相应元素的引用,从而让相应的空间得以回收。
Vector vec = new Vector();
for (int i = 1; i < 100; i++)
{
Object obj = new Object();
vec.add(obj);
// 使用 obj 的相关业务逻辑
obj = null;
}
// 使用 vec 的相关业务逻辑
上面的代码是一个非常经典的例子,乍看之下没有任何问题,每次使用元素后,将元素引用置为 null,保证了 object 空间的回收。 但是,事实上,容器本身随着不断的扩容,也占用着非常大的内存,这是常常被忽略的,如果不将容器本身赋值为 null,则容器本身会在作用域内一直存活。
Vector vec = new Vector();
for (int i = 1; i < 100; i++)
{
Object obj = new Object();
vec.add(obj);
// 使用 obj 的相关业务逻辑
obj = null;
}
// 使用 vec 的相关业务逻辑
vec = null;
改进方法也很简单,在不再使用容器的时候立即赋值为 null 总是最正确的。
public class TestClass implements Cloneable {
private Long value;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class MainClass {
public Set<TestClass> method(List<TestClass> testList)
throws CloneNotSupportedException {
Set<TestClass> result = new HashSet<>();
for (int a = 0; a < 100000) {
for (TestClass test : testList) {
result.add(test.clone());
}
}
}
}
看上去,上述代码实现了对传入的 testList 去重的代码逻辑,虽然重复了很多很多次,但我们的去重代码并不会造成额外的空间浪费。 但是事实上,clone、new 操作都是重新在内存中分配空间,这也就意味着他们的地址是不同的,而所有的类由于都继承了 Object,所以他们的 equals 方法都来源于 Object 类,默认的实现是返回对象地址。 因此,虽然是 clone 得到的对象在 Set 中去重,但是 Set 还是认为他们是不同的对象,从而反复添加造成最终抛出 OutOfMemoryError。
改进方式很简单,对于自定义的类,添加所需的适当 equals 方法的实现即可。
public class TestClass implements Cloneable {
private Long value;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public boolean equals(Object obj) {
return Objects.equals(obj.value, value);
}
}
public class MainClass {
public Set<TestClass> method(List<TestClass> testList)
throws CloneNotSupportedException {
Set<TestClass> result = new HashSet<>();
for (int a = 0; a < 100000) {
for (TestClass test : testList) {
result.add(test.clone());
}
}
}
}