Java的一個重要特性就是通過垃圾收集器(GC)自動管理內存的回收,而不需要程序員自己來釋放內存。理論上Java中所有不會再被利用的對象所占用的內存,都可以被GC回收,但是Java也存在內存泄露,但它的表現與C++不同。
JAVA 中的內存泄露
Java中的內存泄露,廣義並通俗的說,就是:不再會被使用的對象的內存不能被回收,就是內存泄露。
在C++中,所有被分配了內存的對象,不再使用后,都必須程序員手動的釋放他們。所以,每個類,都會含有一個析構函數,作用就是完成清理工作,如果我們忘記了某些對象的釋放,就會造成內存泄露。
但是在Java中,我們不用(也沒辦法)自己釋放內存,無用的對象由GC自動清理,這也極大的簡化了我們的編程工作。但,實際有時候一些不再會被使用的對象,在GC看來不能被釋放,就會造成內存泄露。
我們知道,對象都是有生命周期的,有的長,有的短,如果長生命周期的對象持有短生命周期的引用,就很可能會出現內存泄露。我們舉一個簡單的例子:
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代碼
}
}
這里的object實例,其實我們期望它只作用於method1()方法中,且其他地方不會再用到它,但是,當method1()方法執行完成后,object對象所分配的內存不會馬上被認為是可以被釋放的對象,只有在Simple類創建的對象被釋放后才會被釋放,嚴格的說,這就是一種內存泄露。解決方法就是將object作為method1()方法中的局部變量。當然,如果一定要這么寫,可以改為這樣:
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代碼
object = null;
}
}
一些容易發生內存泄露的例子和解決方法
像上面例子中的情況很容易發生,也是我們最容易忽略並引發內存泄露的情況,解決的原則就是盡量減小對象的作用域(比如android studio中,上面的代碼就會發出警告,並給出的建議是將類的成員變量改寫為方法內的局部變量)以及手動設置null值。
LinkedList源碼舉例
至於作用域,需要在我們編寫代碼時多注意;null值的手動設置,我們可以看一下Java容器LinkedList源碼(可參考:Java之LinkedList源碼解讀(JDK 1.8))的刪除指定節點的內部方法:
//刪除指定節點並返回被刪除的元素值
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;
}
除了修改節點間的關聯關系,我們還要做的就是賦值為null的操作,不管GC何時會開始清理,我們都應及時的將無用的對象標記為可被清理的對象。
ArrayList源碼舉例
我們知道Java容器ArrayList是數組實現的(可參考:Java之ArrayList源碼解讀(JDK 1.8)),如果我們要為其寫一個pop()(彈出)方法,可能會是這樣:
public E pop(){
if(size == 0)
return null;
else
return (E) elementData[--size];
}
寫法很簡潔,但這里卻會造成內存溢出:elementData[size-1]
依然持有E類型對象的引用,並且暫時不能被GC回收。我們可以如下修改:
public E pop(){
if(size == 0)
return null;
else{
E e = (E) elementData[--size];
elementData[size] = null;
return e;
}
}
我們寫代碼並不能一味的追求簡潔,首要是保證其正確性。