Java 中的內(nèi)存溢出和內(nèi)存泄露是什么?我給你舉個(gè)有味道的例子?

JAVA中的內(nèi)存溢出和內(nèi)存泄露分別是什么,有什么聯(lián)系和區(qū)別,讓我們來看一看。

內(nèi)存泄漏 & 內(nèi)存溢出

1. 內(nèi)存泄漏(memory leak )

申請(qǐng)了內(nèi)存用完了不釋放,比如一共有 1024M 的內(nèi)存,分配了 521M 的內(nèi)存一直不回收,那么可以用的內(nèi)存只有 521M 了,仿佛泄露掉了一部分;

通俗一點(diǎn)講的話,內(nèi)存泄漏就是【占著茅坑不拉shi】。

2. 內(nèi)存溢出(out of memory)

申請(qǐng)內(nèi)存時(shí),沒有足夠的內(nèi)存可以使用;

通俗一點(diǎn)兒講,一個(gè)廁所就三個(gè)坑,有兩個(gè)站著茅坑不走的(內(nèi)存泄漏),剩下最后一個(gè)坑,廁所表示接待壓力很大,這時(shí)候一下子來了兩個(gè)人,坑位(內(nèi)存)就不夠了,內(nèi)存泄漏變成內(nèi)存溢出了。

可見,內(nèi)存泄漏和內(nèi)存溢出的關(guān)系:內(nèi)存泄露的增多,最終會(huì)導(dǎo)致內(nèi)存溢出。

這是一個(gè)很有味道的例子。

內(nèi)存泄漏

如上圖:

對(duì)象 X 引用對(duì)象 Y,X 的生命周期比 Y 的生命周期長;

那么當(dāng)Y生命周期結(jié)束的時(shí)候,X依然引用著Y,這時(shí)候,垃圾回收期是不會(huì)回收對(duì)象Y的;

如果對(duì)象X還引用著生命周期比較短的A、B、C,對(duì)象A又引用著對(duì)象 a、b、c,這樣就可能造成大量無用的對(duì)象不能被回收,進(jìn)而占據(jù)了內(nèi)存資源,造成內(nèi)存泄漏,直到內(nèi)存溢出。

泄漏的分類

  • 經(jīng)常發(fā)生:發(fā)生內(nèi)存泄露的代碼會(huì)被多次執(zhí)行,每次執(zhí)行,泄露一塊內(nèi)存;

  • 偶然發(fā)生:在某些特定情況下才會(huì)發(fā)生;

  • 一次性:發(fā)生內(nèi)存泄露的方法只會(huì)執(zhí)行一次;

  • 隱式泄露:一直占著內(nèi)存不釋放,直到執(zhí)行結(jié)束;嚴(yán)格的說這個(gè)不算內(nèi)存泄露,因?yàn)樽罱K釋放掉了,但是如果執(zhí)行時(shí)間特別長,也可能會(huì)導(dǎo)致內(nèi)存耗盡。

導(dǎo)致內(nèi)存泄漏的常見原因

1. 循環(huán)過多或死循環(huán),產(chǎn)生大量對(duì)象;

2. 靜態(tài)集合類引起內(nèi)存泄漏,因?yàn)殪o態(tài)集合的生命周期和 JVM 一致,所以靜態(tài)集合引用的對(duì)象不能被釋放;下面這個(gè)例子中,list 是靜態(tài)的,只要 JVM 不停,那么 obj 也一直不會(huì)釋放。

public class OOM {
 static List list = new ArrayList();
 
 public void oomTests(){
  Object obj = new Object();
  
  list.add(obj);
 }
}

3. 單例模式,和靜態(tài)集合導(dǎo)致內(nèi)存泄露的原因類似,因?yàn)閱卫撵o態(tài)特性,它的生命周期和 JVM 的生命周期一樣長,所以如果單例對(duì)象如果持有外部對(duì)象的引用,那么這個(gè)外部對(duì)象也不會(huì)被回收,那么就會(huì)造成內(nèi)存泄漏。

4. 數(shù)據(jù)連接、IO、Socket連接等等,它們必須顯示釋放(用代碼 close 掉),否則不會(huì)被 GC 回收。

try {
 Connection conn = null;
 Class.forName("com.mysql.jdbc.Driver");
 conn = DriverManager.getConnection("url","", "");
 Statement stmt = conn.createStatement() ;
 ResultSet rs = stmt.executeQuery("....") ; 
} catch (Exception e) {
 //異常日志
} finally {
 //1.關(guān)閉結(jié)果集 Statement
 //2.關(guān)閉聲明的對(duì)象 ResultSet
 //3.關(guān)閉連接 Connection
}

5. 內(nèi)部類的對(duì)象被長期持有,那么內(nèi)部類對(duì)象所屬的外部類對(duì)象也不會(huì)被回收。

6. Hash 值發(fā)生改變,比如下面中的這個(gè)類,它的 hashCode 會(huì)隨著變量 x 的變化而變化:

public class ChangeHashCode {
 private int x ;

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + x;
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  ChangeHashCode other = (ChangeHashCode) obj;
  if (x != other.x)
   return false;
  return true;
 }
 //省略 set 、get 方法
}
public class HashSetTests {
 public static void main(String[] args){
  HashSet<ChangeHashCode> hs = new HashSet<ChangeHashCode>();
  
  ChangeHashCode cc = new ChangeHashCode();
  cc.setX(10);//hashCode = 41
  hs.add(cc);
  
  cc.setX(20);//hashCode = 51
  System.out.println("hs.remove = " + hs.remove(cc));//false
  
  hs.add(cc);
  System.out.println("hs.size = " + hs.size());//size = 2
  
 }
}

可以看到,在測(cè)試方法中,當(dāng)元素的 hashCode 發(fā)生改變之后,就再也找不到改變之前的那個(gè)元素了;

這也是 String 為什么被設(shè)置成了不可變類型,我們可以放心地把 String 存入 HashSet,或者把 String 當(dāng)做 HashMap 的 key 值;

當(dāng)我們想把自己定義的類保存到散列表的時(shí)候,需要保證對(duì)象的 hashCode 不可變。

7. 內(nèi)存中加載數(shù)據(jù)量過大;之前項(xiàng)目在一次上線的時(shí)候,應(yīng)用啟動(dòng)奇慢直到夯死,就是因?yàn)榇a中會(huì)加載一個(gè)表中的數(shù)據(jù)到緩存(內(nèi)存)中,測(cè)試環(huán)境只有幾百條數(shù)據(jù),但是生產(chǎn)環(huán)境有幾百萬的數(shù)據(jù)。

會(huì)點(diǎn)代碼的大叔 | 文【原創(chuàng)】

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容