Java語言中的內(nèi)存泄露代碼詳解
Java的一個重要特性就是通過垃圾收集器(GC)自動管理內(nèi)存的回收,而不需要程序員自己來釋放內(nèi)存。理論上Java中所有不會再被利用的對象所占用的內(nèi)存,都可以被GC回收,但是Java也存在內(nèi)存泄露,但它的表現(xiàn)與C++不同。
JAVA中的內(nèi)存管理
要了解Java中的內(nèi)存泄露,首先就得知道Java中的內(nèi)存是如何管理的。
在Java程序中,我們通常使用new為對象分配內(nèi)存,而這些內(nèi)存空間都在堆(Heap)上。
下面看一個示例:
public class Simple { public static void main(String args[]){ Object object1 = new Object();//obj1 Object object2 = new Object();//obj2 object2 = object1; //...此時,obj2是可以被清理的 } }
Java使用有向圖的方式進行內(nèi)存管理:
在有向圖中,我們叫作obj1是可達的,obj2就是不可達的,顯然不可達的可以被清理。
內(nèi)存的釋放,也即清理那些不可達的對象,是由GC決定和執(zhí)行的,所以GC會監(jiān)控每一個對象的狀態(tài),包括申請、引用、被引用和賦值等。釋放對象的根本原則就是對象不會再被使用:
給對象賦予了空值null,之后再沒有調(diào)用過。
另一個是給對象賦予了新值,這樣重新分配了內(nèi)存空間。
通常,會認為在堆上分配對象的代價比較大,但是GC卻優(yōu)化了這一操作:C++中,在堆上分配一塊內(nèi)存,會查找一塊適用的內(nèi)存加以分配,如果對象銷毀,這塊內(nèi)存就可以重用;而Java中,就想一條長的帶子,每分配一個新的對象,Java的“堆指針”就向后移動到尚未分配的區(qū)域。所以,Java分配內(nèi)存的效率,可與C++媲美。
但是這種工作方式有一個問題:如果頻繁的申請內(nèi)存,資源將會耗盡。這時GC就介入了進來,它會回收空間,并使堆中的對象排列更緊湊。這樣,就始終會有足夠大的內(nèi)存空間可以分配。
gc清理時的引用計數(shù)方式:當引用連接至新對象時,引用計數(shù)+1;當某個引用離開作用域或被設(shè)置為null時,引用計數(shù)-1,GC發(fā)現(xiàn)這個計數(shù)為0時,就回收其占用的內(nèi)存。這個開銷會在引用程序的整個生命周期發(fā)生,并且不能處理循環(huán)引用的情況。所以這種方式只是用來說明GC的工作方式,而不會被任何一種Java虛擬機應(yīng)用。
多數(shù)GC采用一種自適應(yīng)的清理方式(加上其他附加的用于提升速度的技術(shù)),主要依據(jù)是找出任何“活”的對象,然后采用“自適應(yīng)的、分代的、停止-復(fù)制、標記-清理”式的垃圾回收器。具體不介紹太多,這不是本文重點。
JAVA中的內(nèi)存泄露
Java中的內(nèi)存泄露,廣義并通俗的說,就是:不再會被使用的對象的內(nèi)存不能被回收,就是內(nèi)存泄露。
Java中的內(nèi)存泄露與C++中的表現(xiàn)有所不同。
在C++中,所有被分配了內(nèi)存的對象,不再使用后,都必須程序員手動的釋放他們。所以,每個類,都會含有一個析構(gòu)函數(shù),作用就是完成清理工作,如果我們忘記了某些對象的釋放,就會造成內(nèi)存泄露。
但是在Java中,我們不用(也沒辦法)自己釋放內(nèi)存,無用的對象由GC自動清理,這也極大的簡化了我們的編程工作。但,實際有時候一些不再會被使用的對象,在GC看來不能被釋放,就會造成內(nèi)存泄露。
我們知道,對象都是有生命周期的,有的長,有的短,如果長生命周期的對象持有短生命周期的引用,就很可能會出現(xiàn)內(nèi)存泄露。我們舉一個簡單的例子:
public class Simple { Object object; public void method1(){ object = new Object(); //...其他代碼 } }
這里的object實例,其實我們期望它只作用于method1()方法中,且其他地方不會再用到它,但是,當method1()方法執(zhí)行完成后,object對象所分配的內(nèi)存不會馬上被認為是可以被釋放的對象,只有在Simple類創(chuàng)建的對象被釋放后才會被釋放,嚴格的說,這就是一種內(nèi)存泄露。解決方法就是將object作為method1()方法中的局部變量。當然,如果一定要這么寫,可以改為這樣:
public class Simple { Object object; public void method1(){ object = new Object(); //...其他代碼 object = null; } }
這樣,之前“newObject()”分配的內(nèi)存,就可以被GC回收。
到這里,Java的內(nèi)存泄露應(yīng)該都比較清楚了。下面再進一步說明:
在堆中的分配的內(nèi)存,在沒有將其釋放掉的時候,就將所有能訪問這塊內(nèi)存的方式都刪掉(如指針重新賦值),這是針對c++等語言的,Java中的GC會幫我們處理這種情況,所以我們無需關(guān)心。
在內(nèi)存對象明明已經(jīng)不需要的時候,還仍然保留著這塊內(nèi)存和它的訪問方式(引用),這是所有語言都有可能會出現(xiàn)的內(nèi)存泄漏方式。編程時如果不小心,我們很容易發(fā)生這種情況,如果不太嚴重,可能就只是短暫的內(nèi)存泄露。
一些容易發(fā)生內(nèi)存泄露的例子和解決方法
像上面例子中的情況很容易發(fā)生,也是我們最容易忽略并引發(fā)內(nèi)存泄露的情況,解決的原則就是盡量減小對象的作用域(比如androidstudio中,上面的代碼就會發(fā)出警告,并給出的建議是將類的成員變量改寫為方法內(nèi)的局部變量)以及手動設(shè)置null值。
至于作用域,需要在我們編寫代碼時多注意;null值的手動設(shè)置,我們可以看一下Java容器LinkedList源碼(可參考:Java之LinkedList源碼解讀(JDK1.8) )的刪除指定節(jié)點的內(nèi)部方法:
//刪除指定節(jié)點并返回被刪除的元素值 E unlink(Node<E> x) { //獲取當前值和前后節(jié)點 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; //如果前一個節(jié)點為空(如當前節(jié)點為首節(jié)點),后一個節(jié)點成為新的首節(jié)點 } else { prev.next = next;//如果前一個節(jié)點不為空,那么他先后指向當前的下一個節(jié)點 x.prev = null; } if (next == null) { last = prev; //如果后一個節(jié)點為空(如當前節(jié)點為尾節(jié)點),當前節(jié)點前一個成為新的尾節(jié)點 } else { next.prev = prev;//如果后一個節(jié)點不為空,后一個節(jié)點向前指向當前的前一個節(jié)點 x.next = null; } x.item = null; size--; modCount++; return element; }
除了修改節(jié)點間的關(guān)聯(lián)關(guān)系,我們還要做的就是賦值為null的操作,不管GC何時會開始清理,我們都應(yīng)及時的將無用的對象標記為可被清理的對象。
我們知道Java容器ArrayList是數(shù)組實現(xiàn)的(可參考:Java之ArrayList源碼解讀(JDK1.8) ),如果我們要為其寫一個pop()(彈出)方法,可能會是這樣:
public E pop(){ if(size == 0) return null; else return (E) elementData[--size]; }
寫法很簡潔,但這里卻會造成內(nèi)存溢出:elementData[size-1]依然持有E類型對象的引用,并且暫時不能被GC回收。我們可以如下修改:
public E pop(){ if(size == 0) return null; else{ E e = (E) elementData[--size]; elementData[size] = null; return e; } }
我們寫代碼并不能一味的追求簡潔,首要是保證其正確性。
容器使用時的內(nèi)存泄露
在很多文章中可能看到一個如下內(nèi)存泄露例子:
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無關(guān)的其他操作 }
這里內(nèi)存泄露指的是在對vector操作完成之后,執(zhí)行下面與vector無關(guān)的代碼時,如果發(fā)生了GC操作,這一系列的object是沒法被回收的,而此處的內(nèi)存泄露可能是短暫的,因為在整個method()方法執(zhí)行完成后,那些對象還是可以被回收。這里要解決很簡單,手動賦值為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無關(guān)的其他操作 }
上面Vector已經(jīng)過時了,不過只是使用老的例子來做內(nèi)存泄露的介紹。我們使用容器時很容易發(fā)生內(nèi)存泄露,就如上面的例子,不過上例中,容器時方法內(nèi)的局部變量,造成的內(nèi)存泄漏影響可能不算很大(但我們也應(yīng)該避免),但是,如果這個容器作為一個類的成員變量,甚至是一個靜態(tài)(static)的成員變量時,就要更加注意內(nèi)存泄露了。
下面也是一種使用容器時可能會發(fā)生的錯誤:
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{ //設(shè)置默認數(shù)組長度為99999更快的發(fā)生OutOfMemoryError List<String> list = new ArrayList<>(99999); }
運行上面的代碼將很快報錯:
3 Exception 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的容器,上面的錯誤是不可能發(fā)生的。這里也推薦一篇本人介紹Java容器的文章:...
容器Set只存放唯一的元素,是通過對象的equals()方法來比較的,但是Java中所有類都直接或間接繼承至Object類,Object類的equals()方法比較的是對象的地址,上例中,就會一直添加元素直到內(nèi)存溢出。
所以,上例嚴格的說是容器的錯誤使用導(dǎo)致的內(nèi)存溢出。
就Set而言,remove()方法也是通過equals()方法來刪除匹配的元素的,如果一個對象確實提供了正確的equals()方法,但是切記不要在修改這個對象后使用remove(Objecto),這也可能會發(fā)生內(nèi)存泄露。
各種提供了close()方法的對象
比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接,以及使用其他框架的時候,除非其顯式的調(diào)用了其close()方法(或類似方法)將其連接關(guān)閉,否則是不會自動被GC回收的。其實原因依然是長生命周期對象持有短生命周期對象的引用。
可能很多人使用過Hibernate,我們操作數(shù)據(jù)庫時,通過SessionFactory獲取一個session:
Session session=sessionFactory.openSession();
完成后我們必須調(diào)用close()方法關(guān)閉:
session.close();
SessionFactory就是一個長生命周期的對象,而session相對是個短生命周期的對象,但是框架這么設(shè)計是合理的:它并不清楚我們要使用session到多久,于是只能提供一個方法讓我們自己決定何時不再使用。
因為在close()方法調(diào)用之前,可能會拋出異常而導(dǎo)致方法不能被調(diào)用,我們通常使用try語言,然后再finally語句中執(zhí)行close()等清理工作:
try{ session=sessionFactory.openSession(); //...其他操作 }finally{ session.close(); }
單例模式導(dǎo)致的內(nèi)存泄露
單例模式,很多時候我們可以把它的生命周期與整個程序的生命周期看做差不多的,所以是一個長生命周期的對象。如果這個對象持有其他對象的引用,也很容易發(fā)生內(nèi)存泄露。
內(nèi)部類和外部模塊的引用
其實原理依然是一樣的,只是出現(xiàn)的方式不一樣而已。
與清理相關(guān)的方法
本節(jié)主要談?wù)揼c()和finalize()方法。
gc()
對于程序員來說,GC基本是透明的,不可見的。運行GC的函數(shù)是System.gc(),調(diào)用后啟動垃圾回收器開始清理。
但是根據(jù)Java語言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行。因為,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級別較低。
JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達一定程度時,GC才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。除非在一些特定的場合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對于基于Web的實時系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpotJVM就支持這一特性。
finalize()
finalize()是Object類中的方法。
了解C++的都知道有個析構(gòu)函數(shù),但是注意,finalize()絕不等于C++中的析構(gòu)函數(shù)。
Java編程思想中是這么解釋的:一旦GC準備好釋放對象所占用的的存儲空間,將先調(diào)用其finalize()方法,并在下一次GC回收動作發(fā)生時,才會真正回收對象占用的內(nèi)存,所以一些清理工作,我們可以放到finalize()中。
該方法的一個重要的用途是:當在java中調(diào)用非java代碼(如c和c++)時,在這些非java代碼中可能會用到相應(yīng)的申請內(nèi)存的操作(如c的malloc()函數(shù)),而在這些非java代碼中并沒有有效的釋放這些內(nèi)存,就可以使用finalize()方法,并在里面調(diào)用本地方法的free()等函數(shù)。
所以finalize()并不適合用作普通的清理工作。
不過有時候,該方法也有一定的用處:
如果存在一系列對象,對象中有一個狀態(tài)為false,如果我們已經(jīng)處理過這個對象,狀態(tài)會變?yōu)閠rue,為了避免有被遺漏而沒有處理的對象,就可以使用finalize()方法:
class MyObject{ boolean state = false; public void deal(){ //...一些處理操作 state = true; } @Override protected void finalize(){ if(!state){ System.out.println("ERROR:" + "對象未處理!"); } } //... }
但是從很多方面了解,該方法都是被推薦不要使用的,并被認為是多余的。
總的來說,內(nèi)存泄露問題,還是編碼不認真導(dǎo)致的,我們并不能責(zé)怪JVM沒有更合理的清理。
總結(jié)
以上就是本文關(guān)于Java語言中的內(nèi)存泄露代碼詳解的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
相關(guān)文章
Spring和MyBatis整合自動生成代碼里面text類型遇到的坑
Spring和MyBatis整合以后,使用自動生成代碼工具生成dao和mapper配置文件。下面通過本文給大家介紹Spring和MyBatis整合自動生成代碼里面text類型遇到的坑,需要的朋友參考下吧2018-01-01springboot多環(huán)境配置方案(不用5分鐘)
這篇文章主要介紹了springboot多環(huán)境配置方案(不用5分鐘),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01Spring AOP如何整合redis(注解方式)實現(xiàn)緩存統(tǒng)一管理詳解
這篇文章主要給大家介紹了關(guān)于Spring AOP如何整合redis(注解方式)實現(xiàn)緩存統(tǒng)一管理的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08java如何通過FileOutputStream字節(jié)流向文件中寫數(shù)據(jù)
這篇文章主要介紹了java如何通過FileOutputStream字節(jié)流向文件中寫數(shù)據(jù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12Springboot工具類FileCopyUtils使用教程
這篇文章主要介紹了Springboot內(nèi)置的工具類之FileCopyUtils的使用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12