so true

          心懷未來,開創(chuàng)未來!
          隨筆 - 160, 文章 - 0, 評論 - 40, 引用 - 0
          數(shù)據(jù)加載中……

          [轉(zhuǎn)載]Java垃圾收集算法

          1.垃圾收集算法的核心思想

          Java語言建立了垃圾收集機制,用以跟蹤正在使用的對象和發(fā)現(xiàn)并回收不再使用(引用)的對象。該機制可以有效防范動態(tài)內(nèi)存分配中可能發(fā)生的兩個危險:因內(nèi)存垃圾過多而引發(fā)的內(nèi)存耗盡,以及不恰當(dāng)?shù)膬?nèi)存釋放所造成的內(nèi)存非法引用。

          垃圾收集算法的核心思想是:對虛擬機可用內(nèi)存空間,即堆空間中的對象進(jìn)行識別,如果對象正在被引用,那么稱其為存活對象,反之,如果對象不再被引用,則為垃圾對象,可以回收其占據(jù)的空間,用于再分配。垃圾收集算法的選擇和垃圾收集系統(tǒng)參數(shù)的合理調(diào)節(jié)直接影響著系統(tǒng)性能,因此需要開發(fā)人員做比較深入的了解。



          2.觸發(fā)主GC(Garbage Collector)的條件

          JVM進(jìn)行次GC的頻率很高,但因為這種GC占用時間極短,所以對系統(tǒng)產(chǎn)生的影響不大。更值得關(guān)注的是主GC的觸發(fā)條件,因為它對系統(tǒng)影響很明顯。總的來說,有兩個條件會觸發(fā)主GC:

          ①當(dāng)應(yīng)用程序空閑時,即沒有應(yīng)用線程在運行時,GC會被調(diào)用。因為GC在優(yōu)先級最低的線程中進(jìn)行,所以當(dāng)應(yīng)用忙時,GC線程就不會被調(diào)用,但以下條件除外。

          ②Java堆內(nèi)存不足時,GC會被調(diào)用。當(dāng)應(yīng)用線程在運行,并在運行過程中創(chuàng)建新對象,若這時內(nèi)存空間不足,JVM就會強制地調(diào)用GC線程,以便回收內(nèi)存用于新的分配。若GC一次之后仍不能滿足內(nèi)存分配的要求,JVM會再進(jìn)行兩次GC作進(jìn)一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應(yīng)用將停止。

          由于是否進(jìn)行主GC由JVM根據(jù)系統(tǒng)環(huán)境決定,而系統(tǒng)環(huán)境在不斷的變化當(dāng)中,所以主GC的運行具有不確定性,無法預(yù)計它何時必然出現(xiàn),但可以確定的是對一個長期運行的應(yīng)用來說,其主GC是反復(fù)進(jìn)行的。



          3.減少GC開銷的措施

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

          (1)不要顯式調(diào)用System.gc()

          此函數(shù)建議JVM進(jìn)行主GC,雖然只是建議而非一定,但很多情況下它會觸發(fā)主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數(shù)。



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

          臨時對象在跳出函數(shù)調(diào)用后,會成為垃圾,少用臨時變量就相當(dāng)于減少了垃圾的產(chǎn)生,從而延長了出現(xiàn)上述第二個觸發(fā)條件出現(xiàn)的時間,減少了主GC的機會。



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

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



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

          由于String是固定長的字符串對象,累加String對象時,并非在一個String對象中擴增,而是重新創(chuàng)建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執(zhí)行過程中會產(chǎn)生多個垃圾對象,因為對次作“+”操作時都必須創(chuàng)建新的String對象,但這些過渡對象對系統(tǒng)來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎(chǔ)上進(jìn)行擴增,不會產(chǎn)生中間對象。



          (5)能用基本類型如Int,Long,就不用Integer,Long對象

          基本類型變量占用的內(nèi)存資源比相應(yīng)對象占用的少得多,如果沒有必要,最好使用基本變量。



          (6)盡量少用靜態(tài)對象變量

          靜態(tài)變量屬于全局變量,不會被GC回收,它們會一直占用內(nèi)存。



          (7)分散對象創(chuàng)建或刪除的時間

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



          4.gc與finalize方法

          ⑴gc方法請求垃圾回收

          使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法,都可以請求Java的垃圾回收。需要注意的是,調(diào)用System.gc()也僅僅是一個請求。JVM接受這個消息后,并不是立即做垃圾回收,而只是對幾個垃圾回收算法做了加權(quán),使垃圾回收操作容易發(fā)生,或提早發(fā)生,或回收較多而已。



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

          在JVM垃圾收集器收集一個對象之前 ,一般要求程序調(diào)用適當(dāng)?shù)姆椒ㄡ尫刨Y源,但在沒有明確釋放資源的情況下,Java提供了缺省機制來終止化該對象釋放資源,這個方法就是finalize()。它的原型為:

          protected void finalize() throws Throwable

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

          因此,當(dāng)對象即將被銷毀時,有時需要做一些善后工作。可以把這些操作寫在finalize()方法里。

          protected void finalize()

          {

          // finalization code here

          }



          ⑶代碼示例



          class Garbage

          {

          int index;

          static int count;

          Garbage()

          {

          count++;

          System.out.println("object "+count+" construct");

          setID(count);

          }



          void setID(int id)

          {

          index=id;

          }



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

          {

          System.out.println("object "+index+" is reclaimed");

          }



          public static void main(String[] args)

          {

          new Garbage();

          new Garbage();

          new Garbage();

          new Garbage();

          System.gc(); //請求運行垃圾收集器

          }

          }



          5.Java 內(nèi)存泄漏

          由于采用了垃圾回收機制,任何不可達(dá)對象(對象不再被引用)都可以由垃圾收集線程回收。因此通常說的Java 內(nèi)存泄漏其實是指無意識的、非故意的對象引用,或者無意識的對象保持。無意識的對象引用是指代碼的開發(fā)人員本來已經(jīng)對對象使用完畢,卻因為編碼的錯誤而意外地保存了對該對象的引用(這個引用的存在并不是編碼人員的主觀意愿),從而使得該對象一直無法被垃圾回收器回收掉,這種本來以為可以釋放掉的卻最終未能被釋放的空間可以認(rèn)為是被“泄漏了”。

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


          public class ObjStack {

          private Object[] stack;

          private int index;

          ObjStack(int indexcount) {
          stack = new Object[indexcount];
          index = 0;
          }

          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 = 0;
          Object tempobj;
          ObjStack stack1 = new ObjStack(64);//new一個ObjStack對象,并調(diào)用有參構(gòu)造函數(shù)。分配stack Obj數(shù)組的空間大小為64,可以存64個對象,從0開始存儲。
          while (i < 64)
          {
          tempobj = new Object();//循環(huán)new Obj對象,把每次循環(huán)的對象一一存放在stack Obj數(shù)組中。
          stack1.push(tempobj);
          i++;
          System.out.println("第" + i + "次進(jìn)棧" + "t");
          }
          while (i > 32)
          {
          tempobj = stack1.pop();//這里造成了空間的浪費。
          //正確的pop方法可改成如下所指示,當(dāng)引用被返回后,堆棧刪除對他們的引用,因此垃圾收集器在以后可以回收他們。
          /*
          * public Object pop() {index - -;Object temp = stack [index];stack [index]=null;return temp;}
          */
          i--;
          System.out.println("第" + (64 - i) + "次出棧" + "t");
          }
          }

          }


          如何消除內(nèi)存泄漏

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

          垃圾收集器的作用

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

            雖然內(nèi)存管理可以說是自動化的,但是這并不能使編程人員免受思考內(nèi)存管理問題之苦。例如,分配(以及釋放)內(nèi)存總會有開銷,雖然這種開銷對編程人員來說是不可見的。創(chuàng)建了太多對象的程序?qū)韧瓿赏瑯拥墓δ芏鴦?chuàng)建的對象卻比較少的程序更慢一些(在其他條件相同的情況下)。

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

            概括地說,這就是內(nèi)存托管語言中的內(nèi)存泄漏產(chǎn)生的主要原因:保留下來卻永遠(yuǎn)不再使用的對象引用。

          典型泄漏

            既然我們知道了在Java中確實有可能發(fā)生內(nèi)存泄漏,就讓我們來看一些典型的內(nèi)存泄漏及其原因。

          全局集合

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

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

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

          緩存

            緩存是一種數(shù)據(jù)結(jié)構(gòu),用于快速查找已經(jīng)執(zhí)行的操作的結(jié)果。因此,如果一個操作執(zhí)行起來很慢,對于常用的輸入數(shù)據(jù),就可以將操作的結(jié)果緩存,并在下次調(diào)用該操作時使用緩存的數(shù)據(jù)。

            緩存通常都是以動態(tài)方式實現(xiàn)的,其中新的結(jié)果是在執(zhí)行時添加到緩存中的。典型的算法是:

          檢查結(jié)果是否在緩存中,如果在,就返回結(jié)果。
          如果結(jié)果不在緩存中,就進(jìn)行計算。
          將計算出來的結(jié)果添加到緩存中,以便以后對該操作的調(diào)用可以使用。
            該算法的問題(或者說是潛在的內(nèi)存泄漏)出在最后一步。如果調(diào)用該操作時有相當(dāng)多的不同輸入,就將有相當(dāng)多的結(jié)果存儲在緩存中。很明顯這不是正確的方法。

            為了預(yù)防這種具有潛在破壞性的設(shè)計,程序必須確保對于緩存所使用的內(nèi)存容量有一個上限。因此,更好的算法是:

          檢查結(jié)果是否在緩存中,如果在,就返回結(jié)果。
          如果結(jié)果不在緩存中,就進(jìn)行計算。
          如果緩存所占的空間過大,就移除緩存最久的結(jié)果。
          將計算出來的結(jié)果添加到緩存中,以便以后對該操作的調(diào)用可以使用。
            通過始終移除緩存最久的結(jié)果,我們實際上進(jìn)行了這樣的假設(shè):在將來,比起緩存最久的數(shù)據(jù),最近輸入的數(shù)據(jù)更有可能用到。這通常是一個不錯的假設(shè)。

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

            解決這個問題的另一種方法是使用java.lang.ref.SoftReference類跟蹤緩存中的對象。這種方法保證這些引用能夠被移除,如果虛擬機的內(nèi)存用盡而需要更多堆的話。

          ClassLoader

            Java ClassLoader結(jié)構(gòu)的使用為內(nèi)存泄漏提供了許多可乘之機。正是該結(jié)構(gòu)本身的復(fù)雜性使ClassLoader在內(nèi)存泄漏方面存在如此多的問題。ClassLoader的特別之處在于它不僅涉及“常規(guī)”的對象引用,還涉及元對象引用,比如:字段、方法和類。這意味著只要有對字段、方法、類或ClassLoader的對象的引用,ClassLoader就會駐留在JVM中。因為ClassLoader本身可以關(guān)聯(lián)許多類及其靜態(tài)字段,所以就有許多內(nèi)存被泄漏了。

          確定泄漏的位置

            通常發(fā)生內(nèi)存泄漏的第一個跡象是:在應(yīng)用程序中出現(xiàn)了OutOfMemoryError。這通常發(fā)生在您最不愿意它發(fā)生的生產(chǎn)環(huán)境中,此時幾乎不能進(jìn)行調(diào)試。有可能是因為測試環(huán)境運行應(yīng)用程序的方式與生產(chǎn)系統(tǒng)不完全相同,因而導(dǎo)致泄漏只出現(xiàn)在生產(chǎn)中。在這種情況下,需要使用一些開銷較低的工具來監(jiān)控和查找內(nèi)存泄漏。還需要能夠無需重啟系統(tǒng)或修改代碼就可以將這些工具連接到正在運行的系統(tǒng)上。可能最重要的是,當(dāng)進(jìn)行分析時,需要能夠斷開工具而保持系統(tǒng)不受干擾。

            雖然OutOfMemoryError通常都是內(nèi)存泄漏的信號,但是也有可能應(yīng)用程序確實正在使用這么多的內(nèi)存;對于后者,或者必須增加JVM可用的堆的數(shù)量,或者對應(yīng)用程序進(jìn)行某種更改,使它使用較少的內(nèi)存。但是,在許多情況下,OutOfMemoryError都是內(nèi)存泄漏的信號。一種查明方法是不間斷地監(jiān)控GC的活動,確定內(nèi)存使用量是否隨著時間增加。如果確實如此,就可能發(fā)生了內(nèi)存泄漏。

          posted on 2008-01-02 01:56 so true 閱讀(246) 評論(0)  編輯  收藏 所屬分類: Java

          主站蜘蛛池模板: 阿合奇县| 石嘴山市| 大名县| 乌兰浩特市| 五家渠市| 广昌县| 巫山县| 旺苍县| 冷水江市| 蕲春县| 苏尼特右旗| 邯郸市| 井研县| 林州市| 武夷山市| 湖口县| 深水埗区| 浏阳市| 合水县| 阳新县| 临猗县| 大余县| 苏尼特左旗| 同心县| 浦东新区| 安乡县| 精河县| 平顺县| 垦利县| 永昌县| 斗六市| 禄劝| 深州市| 大连市| 康保县| 峨眉山市| 南木林县| 安乡县| 阜宁县| 高阳县| 北海市|