1. 概述
java 語言的一個重要的特性就是垃圾收集器的自動收集和回收,而不需要我們手動去管理和釋放記憶體,這也讓 java 記憶體洩漏問題更加難以發現和處理。
如果你的程式丟擲了 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,那麼通常這就是因為記憶體洩露引起的。
2. 什麼是記憶體洩露
總的來說,釋放物件的原則就是他再也不會被使用,給一個物件賦值為 null 或者其他物件,就會讓這個物件原來所指向的空間變得無法訪問,也就再也無法被使用從而等待 GC 的回收。 記憶體洩露指的就是雖然這部分物件的記憶體已經不會再被使用,但是他們卻不會被 jvm 回收。
- 通常,如果長生命週期的物件持有短生命週期的引用,就很可能會出現記憶體洩露
3. 作用域過大造成的記憶體洩露
3.1. 問題描述
public class Simple {
private Object object;
public void method() {
object = new Object();
// ...
}
}
複製程式碼
以上的程式碼中,我們在 method 方法中為類成員變數 object 賦值了例項化後的值,但是如果我們僅僅在這個方法中使用到了 object,那將意味著在整個類的生命週期中,object 所佔用的空間雖然都不會被再次使用,但卻始終無法得以回收,這就造成了記憶體洩露,如果 object 是一個加入了很多元素的容器,則問題將暴露的更加明顯。
3.2. 改進
上述記憶體洩露程式碼的改進比較簡單。
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:預防為主,治療為輔