JAVA 中的內存泄露
Java中的內存泄露,廣義並通俗的說,就是:不再會被使用的對象的內存不能被回收,就是內存泄露。
Java中的內存泄露與C++中的表現有所不同。
在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; } }
這樣,之前“new Object()”分配的內存,就可以被GC回收。
到這里,Java的內存泄露應該都比較清楚了。下面再進一步說明:
在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值),這是針對c++等語言的,Java中的GC會幫我們處理這種情況,所以我們無需關心。
在內存對象明明已經不需要的時候,還仍然保留着這塊內存和它的訪問方式(引用),這是所有語言都有可能會出現的內存泄漏方式。編程時如果不小心,我們很容易發生這種情況,如果不太嚴重,可能就只是短暫的內存泄露。
一些容易發生內存泄露的例子和解決方法
像上面例子中的情況很容易發生,也是我們最容易忽略並引發內存泄露的情況,解決的原則就是盡量減小對象的作用域(比如android studio中,上面的代碼就會發出警告,並給出的建議是將類的成員變量改寫為方法內的局部變量)以及手動設置null值。
至於作用域,需要在我們編寫代碼時多注意;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; }
容器使用時的內存泄露
在很多文章中可能看到一個如下內存泄露例子:
Vector v = new Vector(); for (int i = 1; i<100; i++){ Object o = new Object(); v.add(o); o = null; }
這里內存泄露指的是在對vector操作完成之后,執行下面與vector無關的代碼時,如果發生了GC操作,這一系列的object是沒法被回收的,而此處的內存泄露可能是短暫的,因為在整個method()方法執行完成后,那些對象還是可以被回收。這里要解決很簡單,手動賦值為null即可:
Vector v = new Vector(); for (int i = 1; i<100; i++){ Object o = new Object(); v.add(o); o = null; } v = null;
上面Vector已經過時了,不過只是使用老的例子來做內存泄露的介紹。我們使用容器時很容易發生內存泄露,就如上面的例子,不過上例中,容器時方法內的局部變量,造成的內存泄漏影響可能不算很大(但我們也應該避免),但是,如果這個容器作為一個類的成員變量,甚至是一個靜態(static)的成員變量時,就要更加注意內存泄露了。
單例模式導致的內存泄露
單例模式,很多時候我們可以把它的生命周期與整個程序的生命周期看做差不多的,所以是一個長生命周期的對象。如果這個對象持有其他對象的引用,也很容易發生內存泄露。
例如: 下面這種常見的寫法,傳了一個Context 進去
import android.content.Context; public class Utils { private Context mContext; private static Utils utils; private Utils(Context mContext) { this.mContext = mContext; } public static Utils getInstance(Context mContext) { if (utils == null) { synchronized (Utils.class) { if (utils == null) { utils = new Utils(mContext); } } } return utils; } }
當我們使用的時候:
public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Utils.getInstance(this); } }
我們在Activity中創建了一個SingleInstance,並且將Activity的實例this傳遞給了該類的對象,導致該單例對象持有了對應的Activity的引用。當我們Activity退出后,由於SingleInstance還存在,它的生命周期並沒有結束,所以SingleInstance依然持有對Activity實例的引用,由於Activity有被引用,導致Activity的實例不能被回收,Activity會長時間的存在內存中。
解決方案:使用弱引用。