Java的一個重要特性就是通過垃圾收集器(GC)自動管理內存的回收,而不需要程序員自己來釋放內存。理論上Java中所有不會再被利用的對象所佔用的內存,都可以被GC回收,但是Java也存在內存洩露,但它的表現與C++不同。
JAVA中的內存管理
要了解Java中的內存洩露,首先就得知道Java中的內存是如何管理的。
在Java程序中,我們通常使用new為對象分配內存,而這些內存空間都在堆(Heap)上。
下面看一個示例:
public class Simple { public static void main(String args[]){ Object object1 = new Object();//obj1 Object object2 = new Object();//obj2 object2 = object1; //...此時,obj2是可以被清理的}}Java使用有向圖的方式進行內存管理:
在有向圖中,我們叫作obj1是可達的,obj2就是不可達的,顯然不可達的可以被清理。
內存的釋放,也即清理那些不可達的對象,是由GC決定和執行的,所以GC會監控每一個對象的狀態,包括申請、引用、被引用和賦值等。釋放對象的根本原則就是對像不會再被使用:
給對象賦予了空值null,之後再沒有調用過。
另一個是給對象賦予了新值,這樣重新分配了內存空間。
通常,會認為在堆上分配對象的代價比較大,但是GC卻優化了這一操作:C++中,在堆上分配一塊內存,會查找一塊適用的內存加以分配,如果對象銷毀,這塊內存就可以重用;而Java中,就想一條長的帶子,每分配一個新的對象,Java的“堆指針”就向後移動到尚未分配的區域。所以,Java分配內存的效率,可與C++媲美。
但是這種工作方式有一個問題:如果頻繁的申請內存,資源將會耗盡。這時GC就介入了進來,它會回收空間,並使堆中的對象排列更緊湊。這樣,就始終會有足夠大的內存空間可以分配。
gc清理時的引用計數方式:當引用連接至新對象時,引用計數+1;當某個引用離開作用域或被設置為null時,引用計數-1,GC發現這個計數為0時,就回收其占用的內存。這個開銷會在引用程序的整個生命週期發生,並且不能處理循環引用的情況。所以這種方式只是用來說明GC的工作方式,而不會被任何一種Java虛擬機應用。
多數GC採用一種自適應的清理方式(加上其他附加的用於提升速度的技術),主要依據是找出任何“活”的對象,然後採用“自適應的、分代的、停止-複製、標記-清理”式的垃圾回收器。具體不介紹太多,這不是本文重點。
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; }}這樣,之前“newObject()”分配的內存,就可以被GC回收。
到這裡,Java的內存洩露應該都比較清楚了。下面再進一步說明:
在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值),這是針對c++等語言的,Java中的GC會幫我們處理這種情況,所以我們無需關心。
在內存對像明明已經不需要的時候,還仍然保留著這塊內存和它的訪問方式(引用),這是所有語言都有可能會出現的內存洩漏方式。編程時如果不小心,我們很容易發生這種情況,如果不太嚴重,可能就只是短暫的內存洩露。
一些容易發生內存洩露的例子和解決方法
像上面例子中的情況很容易發生,也是我們最容易忽略並引發內存洩露的情況,解決的原則就是盡量減小對象的作用域(比如androidstudio中,上面的代碼就會發出警告,並給出的建議是將類的成員變量改寫為方法內的局部變量)以及手動設置null值。
至於作用域,需要在我們編寫代碼時多注意;null值的手動設置,我們可以看一下Java容器LinkedList源碼(可參考: Java之LinkedList源碼解讀(JDK1.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何時會開始清理,我們都應及時的將無用的對象標記為可被清理的對象。
我們知道Java容器ArrayList是數組實現的(可參考: Java之ArrayList源碼解讀(JDK1.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; } }我們寫代碼並不能一味的追求簡潔,首要是保證其正確性。
容器使用時的內存洩露
在很多文章中可能看到一個如下內存洩露例子:
Vector v = new Vector(); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; }可能很多人一開始並不理解,下面我們將上面的代碼完整一下就好理解了:
void method(){ Vector vector = new Vector(); for (int i = 1; i<100; i++) { Object object = new Object(); vector.add(object); object = null; } //...對vector的操作//...與vector無關的其他操作}這里內存洩露指的是在對vector操作完成之後,執行下面與vector無關的代碼時,如果發生了GC操作,這一系列的object是沒法被回收的,而此處的內存洩露可能是短暫的,因為在整個method()方法執行完成後,那些對像還是可以被回收。這裡要解決很簡單,手動賦值為null即可:
void method(){ Vector vector = new Vector(); for (int i = 1; i<100; i++) { Object object = new Object(); vector.add(object); object = null; } //...對v的操作vector = null; //...與v無關的其他操作}上面Vector已經過時了,不過只是使用老的例子來做內存洩露的介紹。我們使用容器時很容易發生內存洩露,就如上面的例子,不過上例中,容器時方法內的局部變量,造成的內存洩漏影響可能不算很大(但我們也應該避免),但是,如果這個容器作為一個類的成員變量,甚至是一個靜態(static)的成員變量時,就要更加註意內存洩露了。
下面也是一種使用容器時可能會發生的錯誤:
public class CollectionMemory { public static void main(String s[]){ Set<MyObject> objects = new LinkedHashSet<MyObject>(); objects.add(new MyObject()); objects.add(new MyObject()); objects.add(new MyObject()); System.out.println(objects.size()); while(true){ objects.add(new MyObject()); } }}class MyObject{ //設置默認數組長度為99999更快的發生OutOfMemoryError List<String> list = new ArrayList<>(99999);}運行上面的代碼將很快報錯:
3Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.ArrayList.<init>(ArrayList.java:152) at com.anxpp.memory.MyObject.<init>(CollectionMemory.java:21) at com.anxpp.memory.CollectionMemory.main(CollectionMemory.java:16)
如果足夠了解Java的容器,上面的錯誤是不可能發生的。這裡也推荐一篇本人介紹Java容器的文章:...
容器Set只存放唯一的元素,是通過對象的equals()方法來比較的,但是Java中所有類都直接或間接繼承至Object類,Object類的equals()方法比較的是對象的地址,上例中,就會一直添加元素直到內存溢出。
所以,上例嚴格的說是容器的錯誤使用導致的內存溢出。
就Set而言,remove()方法也是通過equals()方法來刪除匹配的元素的,如果一個對象確實提供了正確的equals()方法,但是切記不要在修改這個對像後使用remove(Objecto),這也可能會發生內存洩露。
各種提供了close()方法的對象
比如數據庫連接(dataSourse.getConnection()),網絡連接(socket)和io連接,以及使用其他框架的時候,除非其顯式的調用了其close()方法(或類似方法)將其連接關閉,否則是不會自動被GC回收的。其實原因依然是長生命週期對象持有短生命週期對象的引用。
可能很多人使用過Hibernate,我們操作數據庫時,通過SessionFactory獲取一個session:
Session session=sessionFactory.openSession();
完成後我們必須調用close()方法關閉:
session.close();
SessionFactory就是一個長生命週期的對象,而session相對是個短生命週期的對象,但是框架這麼設計是合理的:它並不清楚我們要使用session到多久,於是只能提供一個方法讓我們自己決定何時不再使用。
因為在close()方法調用之前,可能會拋出異常而導致方法不能被調用,我們通常使用try語言,然後再finally語句中執行close()等清理工作:
try{ session=sessionFactory.openSession(); //...其他操作}finally{ session.close(); }單例模式導致的內存洩露
單例模式,很多時候我們可以把它的生命週期與整個程序的生命週期看做差不多的,所以是一個長生命週期的對象。如果這個對象持有其他對象的引用,也很容易發生內存洩露。
內部類和外部模塊的引用
其實原理依然是一樣的,只是出現的方式不一樣而已。
與清理相關的方法
本節主要談論gc()和finalize()方法。
gc()
對於程序員來說,GC基本是透明的,不可見的。運行GC的函數是System.gc(),調用後啟動垃圾回收器開始清理。
但是根據Java語言規範定義,該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。
JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpotJVM就支持這一特性。
finalize()
finalize()是Object類中的方法。
了解C++的都知道有個析構函數,但是注意,finalize()絕不等於C++中的析構函數。
Java編程思想中是這麼解釋的:一旦GC準備好釋放對象所佔用的的存儲空間,將先調用其finalize()方法,並在下一次GC回收動作發生時,才會真正回收對象佔用的內存,所以一些清理工作,我們可以放到finalize()中。
該方法的一個重要的用途是:當在java中調用非java代碼(如c和c++)時,在這些非java代碼中可能會用到相應的申請內存的操作(如c的malloc()函數),而在這些非java代碼中並沒有有效的釋放這些內存,就可以使用finalize()方法,並在裡面調用本地方法的free()等函數。
所以finalize()並不適合用作普通的清理工作。
不過有時候,該方法也有一定的用處:
如果存在一系列對象,對像中有一個狀態為false,如果我們已經處理過這個對象,狀態會變為true,為了避免有被遺漏而沒有處理的對象,就可以使用finalize()方法:
class MyObject{ boolean state = false; public void deal(){ //...一些處理操作state = true; } @Override protected void finalize(){ if(!state){ System.out.println("ERROR:" + "對象未處理!"); } } //...}但是從很多方面了解,該方法都是被推薦不要使用的,並被認為是多餘的。
總的來說,內存洩露問題,還是編碼不認真導致的,我們並不能責怪JVM沒有更合理的清理。
總結
以上就是本文關於Java語言中的內存洩露代碼詳解的全部內容,希望對大家有所幫助。感興趣的朋友可以繼續參閱本站其他相關專題,如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!