熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java垃圾收集算法與內存洩露

2022-06-13   來源: Java核心技術 

  垃圾收集算法的核心思想

  Java語言建立了垃圾收集機制用以跟蹤正在使用的對象和發現並回收不再使用(引用)的對象該機制可以有效防范動態內存分配中可能發生的兩個危險因內存垃圾過多而引發的內存耗盡以及不恰當的內存釋放所造成的內存非法引用

  垃圾收集算法的核心思想是對虛擬機可用內存空間即堆空間中的對象進行識別如果對象正在被引用那麼稱其為存活對象反之如果對象不再被引用則為垃圾對象可以回收其占據的空間用於再分配垃圾收集算法的選擇和垃圾收集系統參數的合理調節直接影響著系統性能因此需要開發人員做比較深入的了解

  觸發主GC(Garbage Collector)的條件

  JVM進行次GC的頻率很高但因為這種GC占用時間極短所以對系統產生的影響不大更值得關注的是主GC的觸發條件因為它對系統影響很明顯總的來說有兩個條件會觸發主GC:

  ①當應用程序空閒時即沒有應用線程在運行時GC會被調用因為GC在優先級最低的線程中進行所以當應用忙時GC線程就不會被調用但以下條件除外

  ②Java堆內存不足時GC會被調用當應用線程在運行並在運行過程中創建新對象若這時內存空間不足JVM就會強制地調用GC線程以便回收內存用於新的分配若GC一次之後仍不能滿足內存分配的要求JVM會再進行兩次GC作進一步的嘗試若仍無法滿足要求則 JVM將報out of memory的錯誤Java應用將停止

  由於是否進行主GC由JVM根據系統環境決定而系統環境在不斷的變化當中所以主GC的運行具有不確定性無法預計它何時必然出現但可以確定的是對一個長期運行的應用來說其主GC是反復進行的

  減少GC開銷的措施

  根據上述GC的機制程序的運行會直接影響系統環境的變化從而影響GC的觸發若不針對GC的特點進行設計和編碼就會出現內存駐留等一系列負面影響為了避免這些影響基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷具體措施包括以下幾個方面:

  ()不要顯式調用Systemgc()

  此函數建議JVM進行主GC雖然只是建議而非一定但很多情況下它會觸發主GC從而增加主GC的頻率也即增加了間歇性停頓的次數

  ()盡量減少臨時對象的使用

  臨時對象在跳出函數調用後會成為垃圾少用臨時變量就相當於減少了垃圾的產生從而延長了出現上述第二個觸發條件出現的時間減少了主GC的機會

  ()對象不用時最好顯式置為Null

  一般而言為Null的對象都會被作為垃圾處理所以將不用的對象顯式地設為Null有利於GC收集器判定垃圾從而提高了GC的效率

  ()盡量使用StringBuffer而不用String來累加字符串(詳見blog另一篇文章JAVA中String與StringBuffer)

  由於String是固定長的字符串對象累加String對象時並非在一個String對象中擴增而是重新創建新的String對象如Str=Str+Str+Str+Str這條語句執行過程中會產生多個垃圾對象因為對次作+操作時都必須創建新的String對象但這些過渡對象對系統來說是沒有實際意義的只會增加更多的垃圾避免這種情況可以改用StringBuffer來累加字符串因StringBuffer是可變長的它在原有基礎上進行擴增不會產生中間對象

  ()能用基本類型如IntLong就不用IntegerLong對象

  基本類型變量占用的內存資源比相應對象占用的少得多如果沒有必要最好使用基本變量

  ()盡量少用靜態對象變量

  靜態變量屬於全局變量不會被GC回收它們會一直占用內存

  ()分散對象創建或刪除的時間

  集中在短時間內大量創建新對象特別是大對象會導致突然需要大量內存JVM在面臨這種情況時只能進行主GC以回收內存或整合內存碎片從而增加主GC的頻率集中刪除對象道理也是一樣的它使得突然出現了大量的垃圾對象空閒空間必然減少從而大大增加了下一次創建新對象時強制主GC的機會

  gc與finalize方法

  ⑴gc方法請求垃圾回收

  使用Systemgc()可以不管JVM使用的是哪一種垃圾回收的算法都可以請求Java的垃圾回收需要注意的是調用Systemgc()也僅僅是一個請求JVM接受這個消息後並不是立即做垃圾回收而只是對幾個垃圾回收算法做了加權使垃圾回收操作容易發生或提早發生或回收較多而已

  ⑵finalize方法透視垃圾收集器的運行

  在JVM垃圾收集器收集一個對象之前 一般要求程序調用適當的方法釋放資源但在沒有明確釋放資源的情況下Java提供了缺省機制來終止化該對象釋放資源這個方法就是finalize()它的原型為

  protected void finalize() throws Throwable

  在finalize()方法返回之後對象消失垃圾收集開始執行原型中的throws Throwable表示它可以拋出任何類型的異常

  因此當對象即將被銷毀時有時需要做一些善後工作可以把這些操作寫在finalize()方法裡

  java 代碼

  protected void finalize()

  {

  // finalization code here

  }

  ⑶代碼示例

  java 代碼

  class Garbage{

  int index;

  static int count;

  Garbage() {

  count++;

  Systemoutprintln(object +count+ construct);

  setID(count);

  }

  void setID(int id) {

  index=id;

  }

  protected void finalize() //重寫finalize方法

  {

  Systemoutprintln(object +index+ is reclaimed);

  }

  public static void main(String[] args)

  {

  new Garbage();

  new Garbage();

  new Garbage();

  new Garbage();

  Systemgc(); //請求運行垃圾收集器

  }

  }

  Java 內存洩漏

  由於采用了垃圾回收機制任何不可達對象(對象不再被引用)都可以由垃圾收集線程回收因此通常說的Java 內存洩漏其實是指無意識的非故意的對象引用或者無意識的對象保持無意識的對象引用是指代碼的開發人員本來已經對對象使用完畢卻因為編碼的錯誤而意外地保存了對該對象的引用(這個引用的存在並不是編碼人員的主觀意願)從而使得該對象一直無法被垃圾回收器回收掉這種本來以為可以釋放掉的卻最終未能被釋放的空間可以認為是被洩漏了

  考慮下面的程序在ObjStack類中使用push和pop方法來管理堆棧中的對象兩個方法中的索引(index)用於指示堆棧中下一個可用位置push方法存儲對新對象的引用並增加索引值而pop方法減小索引值並返回堆棧最上面的元素在main方法中創建了容量為的棧次調用push方法向它添加對象此時index的值為隨後又次調用pop方法則index的值變為出棧意味著在堆棧中的空間應該被收集但事實上pop方法只是減小了索引值堆棧仍然保持著對那些對象的引用個無用對象不會被GC回收造成了內存滲漏

  java 代碼

  public class ObjStack {

  private Object[] stack;

  private int index;

  ObjStack(int indexcount) {

  stack = new Object[indexcount];

  index = ;

  }

  public void push(Object obj) {

  stack[index] = obj;

  index++;

  }

  public Object pop() {

  index;

  return stack[index];

  }

  }

  public class Pushpop {

  public static void main(String[] args) {

  int i = ;

  Object tempobj;

  //new一個ObjStack對象並調用有參構造函數分配stack Obj數組的空間大小為可以存個對象開始存儲

  ObjStack stack = new ObjStack();

  while (i < )

  {

  tempobj = new Object();//循環new Obj對象把每次循環的對象一一存放在stack Obj數組中

  stackpush(tempobj);

  i++;

  Systemoutprintln( + i + 次進棧 + \t);

  }

  while (i > )

  {

  tempobj = stackpop();//這裡造成了空間的浪費

  //正確的pop方法可改成如下所指示當引用被返回後堆棧刪除對他們的引用因此垃圾收集器在以後可以回收他們

  /*

  * public Object pop() {index ;Object temp = stack [index];stack [index]=null;return temp;}

  */

  i;

  Systemoutprintln( + ( i) + 次出棧 + \t);

  }

  }

  }

  如何消除內存洩漏

  雖然Java虛擬機(JVM)及其垃圾收集器(garbage collectorGC)負責管理大多數的內存任務Java軟件程序中還是有可能出現內存洩漏實際上這在大型項目中是一個常見的問題避免內存洩漏的第一步是要弄清楚它是如何發生的本文介紹了編寫Java代碼的一些常見的內存洩漏陷阱以及編寫不洩漏代碼的一些最佳實踐一旦發生了內存洩漏要指出造成洩漏的代碼是非常困難的因此本文還介紹了一種新工具用來診斷洩漏並指出根本原因該工具的開銷非常小因此可以使用它來尋找處於生產中的系統的內存洩漏

  垃圾收集器的作用

  雖然垃圾收集器處理了大多數內存管理問題從而使編程人員的生活變得更輕松了但是編程人員還是可能犯錯而導致出現內存問題簡單地說GC循環地跟蹤所有來自對象(堆棧對象靜態對象JNI句柄指向的對象諸如此類)的引用並將所有它所能到達的對象標記為活動的程序只可以操縱這些對象;其他的對象都被刪除了因為GC使程序不可能到達已被刪除的對象這麼做就是安全的

  雖然內存管理可以說是自動化的但是這並不能使編程人員免受思考內存管理問題之苦例如分配(以及釋放)內存總會有開銷雖然這種開銷對編程人員來說是不可見的創建了太多對象的程序將會比完成同樣的功能而創建的對象卻比較少的程序更慢一些(在其他條件相同的情況下)

  而且與本文更為密切相關的是如果忘記釋放先前分配的內存就可能造成內存洩漏如果程序保留對永遠不再使用的對象的引用這些對象將會占用並耗盡內存這是因為自動化的垃圾收集器無法證明這些對象將不再使用正如我們先前所說的如果存在一個對對象的引用對象就被定義為活動的因此不能刪除為了確保能回收對象占用的內存編程人員必須確保該對象不能到達這通常是通過將對象字段設置為null或者從集合(collection)中移除對象而完成的但是注意當局部變量不再使用時沒有必要將其顯式地設置為null對這些變量的引用將隨著方法的退出而自動清除

  概括地說這就是內存托管語言中的內存洩漏產生的主要原因保留下來卻永遠不再使用的對象引用

  典型洩漏

  既然我們知道了在Java中確實有可能發生內存洩漏就讓我們來看一些典型的內存洩漏及其原因

  全局集合

  在大的應用程序中有某種全局的數據儲存庫是很常見的例如一個JNDI樹或一個會話表在這些情況下必須注意管理儲存庫的大小必須有某種機制從儲存庫中移除不再需要的數據

  這可能有多種方法但是最常見的一種是周期性運行的某種清除任務該任務將驗證儲存庫中的數據並移除任何不再需要的數據

  另一種管理儲存庫的方法是使用反向鏈接(referrer)計數然後集合負責統計集合中每個入口的反向鏈接的數目這要求反向鏈接告訴集合何時會退出入口當反向鏈接數目為零時該元素就可以從集合中移除了

  緩存

  緩存是一種數據結構用於快速查找已經執行的操作的結果因此如果一個操作執行起來很慢對於常用的輸入數據就可以將操作的結果緩存並在下次調用該操作時使用緩存的數據

  緩存通常都是以動態方式實現的其中新的結果是在執行時添加到緩存中的典型的算法是

  檢查結果是否在緩存中如果在就返回結果

  如果結果不在緩存中就進行計算

  將計算出來的結果添加到緩存中以便以後對該操作的調用可以使用

  該算法的問題(或者說是潛在的內存洩漏)出在最後一步如果調用該操作時有相當多的不同輸入就將有相當多的結果存儲在緩存中很明顯這不是正確的方法

  為了預防這種具有潛在破壞性的設計程序必須確保對於緩存所使用的內存容量有一個上限因此更好的算法是

  檢查結果是否在緩存中如果在就返回結果

  如果結果不在緩存中就進行計算

  如果緩存所占的空間過大就移除緩存最久的結果

  將計算出來的結果添加到緩存中以便以後對該操作的調用可以使用

  通過始終移除緩存最久的結果我們實際上進行了這樣的假設在將來比起緩存最久的數據最近輸入的數據更有可能用到這通常是一個不錯的假設

  新算法將確保緩存的容量處於預定義的內存范圍之內確切的范圍可能很難計算因為緩存中的對象在不斷變化而且它們的引用包羅萬象為緩存設置正確的大小是一項非常復雜的任務需要將所使用的內存容量與檢索數據的速度加以平衡

  解決這個問題的另一種方法是使用javalangrefSoftReference類跟蹤緩存中的對象這種方法保證這些引用能夠被移除如果虛擬機的內存用盡而需要更多堆的話

  ClassLoader

  Java ClassLoader結構的使用為內存洩漏提供了許多可乘之機正是該結構本身的復雜性使ClassLoader在內存洩漏方面存在如此多的問題ClassLoader的特別之處在於它不僅涉及常規的對象引用還涉及元對象引用比如字段方法和類這意味著只要有對字段方法類或ClassLoader的對象的引用ClassLoader就會駐留在JVM中因為ClassLoader本身可以關聯許多類及其靜態字段所以就有許多內存被洩漏了

  確定洩漏的位置

  通常發生內存洩漏的第一個跡象是在應用程序中出現了OutOfMemoryError這通常發生在您最不願意它發生的生產環境中此時幾乎不能進行調試有可能是因為測試環境運行應用程序的方式與生產系統不完全相同因而導致洩漏只出現在生產中在這種情況下需要使用一些開銷較低的工具來監控和查找內存洩漏還需要能夠無需重啟系統或修改代碼就可以將這些工具連接到正在運行的系統上可能最重要的是當進行分析時需要能夠斷開工具而保持系統不受干擾

  雖然OutOfMemoryError通常都是內存洩漏的信號但是也有可能應用程序確實正在使用這麼多的內存;對於後者或者必須增加JVM可用的堆的數量或者對應用程序進行某種更改使它使用較少的內存但是在許多情況下OutOfMemoryError都是內存洩漏的信號一種查明方法是不間斷地監控GC的活動確定內存使用量是否隨著時間增加如果確實如此就可能發生了內存洩漏


From:http://tw.wingwit.com/Article/program/Java/hx/201311/25597.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.