溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java語(yǔ)言中的內(nèi)存泄露代碼詳解

發(fā)布時(shí)間:2020-08-27 00:54:22 來(lái)源:腳本之家 閱讀:159 作者:anxpp 欄目:編程語(yǔ)言

Java的一個(gè)重要特性就是通過(guò)垃圾收集器(GC)自動(dòng)管理內(nèi)存的回收,而不需要程序員自己來(lái)釋放內(nèi)存。理論上Java中所有不會(huì)再被利用的對(duì)象所占用的內(nèi)存,都可以被GC回收,但是Java也存在內(nèi)存泄露,但它的表現(xiàn)與C++不同。

Java語(yǔ)言中的內(nèi)存泄露代碼詳解

JAVA中的內(nèi)存管理

要了解Java中的內(nèi)存泄露,首先就得知道Java中的內(nèi)存是如何管理的。

在Java程序中,我們通常使用new為對(duì)象分配內(nèi)存,而這些內(nèi)存空間都在堆(Heap)上。

下面看一個(gè)示例:

public class Simple {
 public static void main(String args[]){
  Object object1 = new Object();//obj1
  Object object2 = new Object();//obj2
  object2 = object1;
  //...此時(shí),obj2是可以被清理的
 }
}

Java使用有向圖的方式進(jìn)行內(nèi)存管理:

Java語(yǔ)言中的內(nèi)存泄露代碼詳解

在有向圖中,我們叫作obj1是可達(dá)的,obj2就是不可達(dá)的,顯然不可達(dá)的可以被清理。

內(nèi)存的釋放,也即清理那些不可達(dá)的對(duì)象,是由GC決定和執(zhí)行的,所以GC會(huì)監(jiān)控每一個(gè)對(duì)象的狀態(tài),包括申請(qǐng)、引用、被引用和賦值等。釋放對(duì)象的根本原則就是對(duì)象不會(huì)再被使用:

給對(duì)象賦予了空值null,之后再?zèng)]有調(diào)用過(guò)。

另一個(gè)是給對(duì)象賦予了新值,這樣重新分配了內(nèi)存空間。

通常,會(huì)認(rèn)為在堆上分配對(duì)象的代價(jià)比較大,但是GC卻優(yōu)化了這一操作:C++中,在堆上分配一塊內(nèi)存,會(huì)查找一塊適用的內(nèi)存加以分配,如果對(duì)象銷毀,這塊內(nèi)存就可以重用;而Java中,就想一條長(zhǎng)的帶子,每分配一個(gè)新的對(duì)象,Java的“堆指針”就向后移動(dòng)到尚未分配的區(qū)域。所以,Java分配內(nèi)存的效率,可與C++媲美。

但是這種工作方式有一個(gè)問(wèn)題:如果頻繁的申請(qǐng)內(nèi)存,資源將會(huì)耗盡。這時(shí)GC就介入了進(jìn)來(lái),它會(huì)回收空間,并使堆中的對(duì)象排列更緊湊。這樣,就始終會(huì)有足夠大的內(nèi)存空間可以分配。

gc清理時(shí)的引用計(jì)數(shù)方式:當(dāng)引用連接至新對(duì)象時(shí),引用計(jì)數(shù)+1;當(dāng)某個(gè)引用離開作用域或被設(shè)置為null時(shí),引用計(jì)數(shù)-1,GC發(fā)現(xiàn)這個(gè)計(jì)數(shù)為0時(shí),就回收其占用的內(nèi)存。這個(gè)開銷會(huì)在引用程序的整個(gè)生命周期發(fā)生,并且不能處理循環(huán)引用的情況。所以這種方式只是用來(lái)說(shuō)明GC的工作方式,而不會(huì)被任何一種Java虛擬機(jī)應(yīng)用。

多數(shù)GC采用一種自適應(yīng)的清理方式(加上其他附加的用于提升速度的技術(shù)),主要依據(jù)是找出任何“活”的對(duì)象,然后采用“自適應(yīng)的、分代的、停止-復(fù)制、標(biāo)記-清理”式的垃圾回收器。具體不介紹太多,這不是本文重點(diǎn)。

JAVA中的內(nèi)存泄露

Java中的內(nèi)存泄露,廣義并通俗的說(shuō),就是:不再會(huì)被使用的對(duì)象的內(nèi)存不能被回收,就是內(nèi)存泄露。

Java中的內(nèi)存泄露與C++中的表現(xiàn)有所不同。

在C++中,所有被分配了內(nèi)存的對(duì)象,不再使用后,都必須程序員手動(dòng)的釋放他們。所以,每個(gè)類,都會(huì)含有一個(gè)析構(gòu)函數(shù),作用就是完成清理工作,如果我們忘記了某些對(duì)象的釋放,就會(huì)造成內(nèi)存泄露。

但是在Java中,我們不用(也沒(méi)辦法)自己釋放內(nèi)存,無(wú)用的對(duì)象由GC自動(dòng)清理,這也極大的簡(jiǎn)化了我們的編程工作。但,實(shí)際有時(shí)候一些不再會(huì)被使用的對(duì)象,在GC看來(lái)不能被釋放,就會(huì)造成內(nèi)存泄露。

我們知道,對(duì)象都是有生命周期的,有的長(zhǎng),有的短,如果長(zhǎng)生命周期的對(duì)象持有短生命周期的引用,就很可能會(huì)出現(xiàn)內(nèi)存泄露。我們舉一個(gè)簡(jiǎn)單的例子:

public class Simple {
 Object object;
 public void method1(){
  object = new Object();
  //...其他代碼
 }
}

這里的object實(shí)例,其實(shí)我們期望它只作用于method1()方法中,且其他地方不會(huì)再用到它,但是,當(dāng)method1()方法執(zhí)行完成后,object對(duì)象所分配的內(nèi)存不會(huì)馬上被認(rèn)為是可以被釋放的對(duì)象,只有在Simple類創(chuàng)建的對(duì)象被釋放后才會(huì)被釋放,嚴(yán)格的說(shuō),這就是一種內(nèi)存泄露。解決方法就是將object作為method1()方法中的局部變量。當(dāng)然,如果一定要這么寫,可以改為這樣:

public class Simple {
 Object object;
 public void method1(){
  object = new Object();
  //...其他代碼
  object = null;
 }
}

這樣,之前“newObject()”分配的內(nèi)存,就可以被GC回收。

到這里,Java的內(nèi)存泄露應(yīng)該都比較清楚了。下面再進(jìn)一步說(shuō)明:

在堆中的分配的內(nèi)存,在沒(méi)有將其釋放掉的時(shí)候,就將所有能訪問(wèn)這塊內(nèi)存的方式都刪掉(如指針重新賦值),這是針對(duì)c++等語(yǔ)言的,Java中的GC會(huì)幫我們處理這種情況,所以我們無(wú)需關(guān)心。

在內(nèi)存對(duì)象明明已經(jīng)不需要的時(shí)候,還仍然保留著這塊內(nèi)存和它的訪問(wèn)方式(引用),這是所有語(yǔ)言都有可能會(huì)出現(xiàn)的內(nèi)存泄漏方式。編程時(shí)如果不小心,我們很容易發(fā)生這種情況,如果不太嚴(yán)重,可能就只是短暫的內(nèi)存泄露。

一些容易發(fā)生內(nèi)存泄露的例子和解決方法

像上面例子中的情況很容易發(fā)生,也是我們最容易忽略并引發(fā)內(nèi)存泄露的情況,解決的原則就是盡量減小對(duì)象的作用域(比如androidstudio中,上面的代碼就會(huì)發(fā)出警告,并給出的建議是將類的成員變量改寫為方法內(nèi)的局部變量)以及手動(dòng)設(shè)置null值。

至于作用域,需要在我們編寫代碼時(shí)多注意;null值的手動(dòng)設(shè)置,我們可以看一下Java容器LinkedList源碼(可參考:Java之LinkedList源碼解讀(JDK1.8) )的刪除指定節(jié)點(diǎn)的內(nèi)部方法:

//刪除指定節(jié)點(diǎn)并返回被刪除的元素值
 E unlink(Node<E> x) {
  //獲取當(dāng)前值和前后節(jié)點(diǎn)
  final E element = x.item;
  final Node<E> next = x.next;
  final Node<E> prev = x.prev;
  if (prev == null) {
   first = next; //如果前一個(gè)節(jié)點(diǎn)為空(如當(dāng)前節(jié)點(diǎn)為首節(jié)點(diǎn)),后一個(gè)節(jié)點(diǎn)成為新的首節(jié)點(diǎn)
  } else {
   prev.next = next;//如果前一個(gè)節(jié)點(diǎn)不為空,那么他先后指向當(dāng)前的下一個(gè)節(jié)點(diǎn)
   x.prev = null;
  }
  if (next == null) {
   last = prev; //如果后一個(gè)節(jié)點(diǎn)為空(如當(dāng)前節(jié)點(diǎn)為尾節(jié)點(diǎn)),當(dāng)前節(jié)點(diǎn)前一個(gè)成為新的尾節(jié)點(diǎn)
  } else {
   next.prev = prev;//如果后一個(gè)節(jié)點(diǎn)不為空,后一個(gè)節(jié)點(diǎn)向前指向當(dāng)前的前一個(gè)節(jié)點(diǎn)
   x.next = null;
  }
  x.item = null;
  size--;
  modCount++;
  return element;
 }

除了修改節(jié)點(diǎn)間的關(guān)聯(lián)關(guān)系,我們還要做的就是賦值為null的操作,不管GC何時(shí)會(huì)開始清理,我們都應(yīng)及時(shí)的將無(wú)用的對(duì)象標(biāo)記為可被清理的對(duì)象。

我們知道Java容器ArrayList是數(shù)組實(shí)現(xiàn)的(可參考:Java之ArrayList源碼解讀(JDK1.8) ),如果我們要為其寫一個(gè)pop()(彈出)方法,可能會(huì)是這樣:

public E pop(){
  if(size == 0)
   return null;
  else
   return (E) elementData[--size];
 }

寫法很簡(jiǎn)潔,但這里卻會(huì)造成內(nèi)存溢出:elementData[size-1]依然持有E類型對(duì)象的引用,并且暫時(shí)不能被GC回收。我們可以如下修改:

 public E pop(){
  if(size == 0)
   return null;
  else{
   E e = (E) elementData[--size];
   elementData[size] = null;
   return e;
  }
 }

我們寫代碼并不能一味的追求簡(jiǎn)潔,首要是保證其正確性。

容器使用時(shí)的內(nèi)存泄露

在很多文章中可能看到一個(gè)如下內(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;
  }
  //...對(duì)vector的操作
  //...與vector無(wú)關(guān)的其他操作
 }

這里內(nèi)存泄露指的是在對(duì)vector操作完成之后,執(zhí)行下面與vector無(wú)關(guān)的代碼時(shí),如果發(fā)生了GC操作,這一系列的object是沒(méi)法被回收的,而此處的內(nèi)存泄露可能是短暫的,因?yàn)樵谡麄€(gè)method()方法執(zhí)行完成后,那些對(duì)象還是可以被回收。這里要解決很簡(jiǎn)單,手動(dòng)賦值為null即可:

 void method(){
  Vector vector = new Vector();
  for (int i = 1; i<100; i++)
  {
   Object object = new Object();
   vector.add(object);
   object = null;
  }
  //...對(duì)v的操作
  vector = null;
  //...與v無(wú)關(guān)的其他操作
 }

上面Vector已經(jīng)過(guò)時(shí)了,不過(guò)只是使用老的例子來(lái)做內(nèi)存泄露的介紹。我們使用容器時(shí)很容易發(fā)生內(nèi)存泄露,就如上面的例子,不過(guò)上例中,容器時(shí)方法內(nèi)的局部變量,造成的內(nèi)存泄漏影響可能不算很大(但我們也應(yīng)該避免),但是,如果這個(gè)容器作為一個(gè)類的成員變量,甚至是一個(gè)靜態(tài)(static)的成員變量時(shí),就要更加注意內(nèi)存泄露了。

下面也是一種使用容器時(shí)可能會(huì)發(fā)生的錯(cuò)誤:

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è)置默認(rèn)數(shù)組長(zhǎng)度為99999更快的發(fā)生OutOfMemoryError
 List<String> list = new ArrayList<>(99999);
}

運(yùn)行上面的代碼將很快報(bào)錯(cuò):

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的容器,上面的錯(cuò)誤是不可能發(fā)生的。這里也推薦一篇本人介紹Java容器的文章:...

容器Set只存放唯一的元素,是通過(guò)對(duì)象的equals()方法來(lái)比較的,但是Java中所有類都直接或間接繼承至Object類,Object類的equals()方法比較的是對(duì)象的地址,上例中,就會(huì)一直添加元素直到內(nèi)存溢出。

所以,上例嚴(yán)格的說(shuō)是容器的錯(cuò)誤使用導(dǎo)致的內(nèi)存溢出。

就Set而言,remove()方法也是通過(guò)equals()方法來(lái)刪除匹配的元素的,如果一個(gè)對(duì)象確實(shí)提供了正確的equals()方法,但是切記不要在修改這個(gè)對(duì)象后使用remove(Objecto),這也可能會(huì)發(fā)生內(nèi)存泄露。

各種提供了close()方法的對(duì)象

比如數(shù)據(jù)庫(kù)連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接,以及使用其他框架的時(shí)候,除非其顯式的調(diào)用了其close()方法(或類似方法)將其連接關(guān)閉,否則是不會(huì)自動(dòng)被GC回收的。其實(shí)原因依然是長(zhǎng)生命周期對(duì)象持有短生命周期對(duì)象的引用。

可能很多人使用過(guò)Hibernate,我們操作數(shù)據(jù)庫(kù)時(shí),通過(guò)SessionFactory獲取一個(gè)session:

Session session=sessionFactory.openSession();

完成后我們必須調(diào)用close()方法關(guān)閉:

session.close();

SessionFactory就是一個(gè)長(zhǎng)生命周期的對(duì)象,而session相對(duì)是個(gè)短生命周期的對(duì)象,但是框架這么設(shè)計(jì)是合理的:它并不清楚我們要使用session到多久,于是只能提供一個(gè)方法讓我們自己決定何時(shí)不再使用。

因?yàn)樵赾lose()方法調(diào)用之前,可能會(huì)拋出異常而導(dǎo)致方法不能被調(diào)用,我們通常使用try語(yǔ)言,然后再finally語(yǔ)句中執(zhí)行close()等清理工作:

 try{
  session=sessionFactory.openSession();
  //...其他操作
 }finally{
  session.close();
 }

單例模式導(dǎo)致的內(nèi)存泄露

單例模式,很多時(shí)候我們可以把它的生命周期與整個(gè)程序的生命周期看做差不多的,所以是一個(gè)長(zhǎng)生命周期的對(duì)象。如果這個(gè)對(duì)象持有其他對(duì)象的引用,也很容易發(fā)生內(nèi)存泄露。

內(nèi)部類和外部模塊的引用

其實(shí)原理依然是一樣的,只是出現(xiàn)的方式不一樣而已。

與清理相關(guān)的方法

本節(jié)主要談?wù)揼c()和finalize()方法。

gc()

對(duì)于程序員來(lái)說(shuō),GC基本是透明的,不可見(jiàn)的。運(yùn)行GC的函數(shù)是System.gc(),調(diào)用后啟動(dòng)垃圾回收器開始清理。

但是根據(jù)Java語(yǔ)言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行。因?yàn)椋煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級(jí)別較低。

JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開始工作,也有定時(shí)執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來(lái)說(shuō),我們不需要關(guān)心這些。除非在一些特定的場(chǎng)合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對(duì)于基于Web的實(shí)時(shí)系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過(guò)平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpotJVM就支持這一特性。

finalize()

finalize()是Object類中的方法。

了解C++的都知道有個(gè)析構(gòu)函數(shù),但是注意,finalize()絕不等于C++中的析構(gòu)函數(shù)。

Java編程思想中是這么解釋的:一旦GC準(zhǔn)備好釋放對(duì)象所占用的的存儲(chǔ)空間,將先調(diào)用其finalize()方法,并在下一次GC回收動(dòng)作發(fā)生時(shí),才會(huì)真正回收對(duì)象占用的內(nèi)存,所以一些清理工作,我們可以放到finalize()中。

該方法的一個(gè)重要的用途是:當(dāng)在java中調(diào)用非java代碼(如c和c++)時(shí),在這些非java代碼中可能會(huì)用到相應(yīng)的申請(qǐng)內(nèi)存的操作(如c的malloc()函數(shù)),而在這些非java代碼中并沒(méi)有有效的釋放這些內(nèi)存,就可以使用finalize()方法,并在里面調(diào)用本地方法的free()等函數(shù)。

所以finalize()并不適合用作普通的清理工作。

不過(guò)有時(shí)候,該方法也有一定的用處:

如果存在一系列對(duì)象,對(duì)象中有一個(gè)狀態(tài)為false,如果我們已經(jīng)處理過(guò)這個(gè)對(duì)象,狀態(tài)會(huì)變?yōu)閠rue,為了避免有被遺漏而沒(méi)有處理的對(duì)象,就可以使用finalize()方法:

class MyObject{
 boolean state = false;
 public void deal(){
  //...一些處理操作
  state = true;
 }
 @Override
 protected void finalize(){
  if(!state){
   System.out.println("ERROR:" + "對(duì)象未處理!");
  }
 }
 //...
}

但是從很多方面了解,該方法都是被推薦不要使用的,并被認(rèn)為是多余的。

總的來(lái)說(shuō),內(nèi)存泄露問(wèn)題,還是編碼不認(rèn)真導(dǎo)致的,我們并不能責(zé)怪JVM沒(méi)有更合理的清理。

總結(jié)

以上就是本文關(guān)于Java語(yǔ)言中的內(nèi)存泄露代碼詳解的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI