1. 概述java
java 语言的一个重要的特性就是垃圾收集器的自动收集和回收,而不须要咱们手动去管理和释放内存,这也让 java 内存泄漏问题更加难以发现和处理。bash
若是你的程序抛出了 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,那么一般这就是由于内存泄露引发的。jvm
2. 什么是内存泄露ide
总的来讲,释放对象的原则就是他不再会被使用,给一个对象赋值为 null 或者其余对象,就会让这个对象原来所指向的空间变得没法访问,也就再也没法被使用从而等待 GC 的回收。 内存泄露指的就是虽然这部分对象的内存已经不会再被使用,可是他们却不会被 jvm 回收。ui
3. 做用域过大形成的内存泄露this
3.1. 问题描述spa
public class Simple {
private Object object;
public void method() {
object = new Object();
// ...
}
}
复制代码
以上的代码中,咱们在 method 方法中为类成员变量 object 赋值了实例化后的值,可是若是咱们仅仅在这个方法中使用到了 object,那将意味着在整个类的生命周期中,object 所占用的空间虽然都不会被再次使用,但却始终没法得以回收,这就形成了内存泄露,若是 object 是一个加入了不少元素的容器,则问题将暴露的更加明显。code
3.2. 改进cdn
上述内存泄露代码的改进比较简单。对象
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 回收,在实际开发中,很容易被忽略。
4. 容器元素形成的内存邪路
4.1. 问题描述
下面是咱们经过 ArrayList 实现的一个 pop 方法。
public E pop(){
if(size == 0)
return null;
else
return (E) elementData[--size];
}
复制代码
实现起来很是简单,可是却存在着内存泄露的问题,由于 size 变小致使 ArrayList 中原有的末端元素将永远得不到使用,可是因为容器持有着他们的引用,他们也永远得不到释放。
4.2. 改进
public E pop(){
if(size == 0)
return null;
else{
E e = (E) elementData[--size];
elementData[size] = null;
return e;
}
}
复制代码
经过主动赋值为 null 从而释放相应元素的引用,从而让相应的空间得以回收。
5. 容器自己形成的内存泄露
5.1. 问题描述
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,则容器自己会在做用域内一直存活。
5.2. 改进
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 老是最正确的。
6. Set、Map 容器使用默认 equals 方法形成的内存泄露
6.1. 问题描述
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。
6.2. 改进
改进方式很简单,对于自定义的类,添加所需的适当 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());
}
}
}
}
复制代码
note:预防为主,治疗为辅