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:預防爲主,治療爲輔