1. 程式人生 > >java對於垃圾回收機制[GC垃圾回收機制] 為什麼有GC還會有記憶體溢位呢?

java對於垃圾回收機制[GC垃圾回收機制] 為什麼有GC還會有記憶體溢位呢?

java垃圾回收機制
來源於書本和工作中的總結。
記憶體洩露
如果分配出去的記憶體得不到釋放,及時回收,就會引起系統執行速度下降,甚至導致程式癱瘓,這就是記憶體洩露
GC機制
java記憶體分配和回收 都是jre後臺進行, 簡稱GC機制,
JRE在回收時做了什麼
jre 會提供一個後臺執行緒 進行檢測和控制, 使用垃圾回收演算法進行(1)發現無用資訊物件;(2)回收被無用物件佔用的記憶體空間,使該空間可被程式再次使用。
回收的時機一般分為:CPU空閒,記憶體不足,記憶體使用極限
垃圾回收機制的缺點,優點,特點,小記
缺點,
無法精確控制垃圾回收的時機和順序,虛擬機器需要跟蹤所有物件,確定有用和無用的物件,花費處理器時間,
優點:
不需要花太多時間 解決 儲存器問題,縮短開發時間;安全性完整性,GC是一套完整機制
特點,
只能回收無用物件的記憶體空間,對於物理資源,如資料庫連結, 磁碟IO流,網路連結等,等物理型別資源無法釋放,需要手動釋放處理。
有關函式
一.System.gc()方法


命令列引數透視垃圾收集器的執行
使用System.gc()可以不管JVM使用的是哪一種垃圾回收的演算法,都可以請求Java的垃圾回收。
在命令列中有一個引數-verbosegc可以檢視Java使用的堆記憶體的情況,它的格式如下:
 
  java -verbosegc classfile
  可以看個例子:

  
  class TestGC  
    {  
        public static void main(String[] args)  
        {  
          new TestGC();  
          System.gc();  
          System.runFinalization();  
       }  
    }  

二. finalize()方法
在JVM垃圾回收器收集一個物件之前,一般要求程式呼叫適當的方法釋放資源,但在沒有明確釋放資源的情況下,Java提供了預設機制來終止該物件心釋放資源,這個方法就是finalize()。它的原型為:
  protected void finalize() throws Throwable
  
  在finalize()方法返回之後,物件消失,垃圾收集開始執行。原型中的throws Throwable表示它可以丟擲任何型別的異常。
  
  之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況。假定你的物件(並非使用new方法)獲得了一塊“特殊”的記憶體區域,由於垃圾回收器只知道那些顯示地經由new分配的記憶體空間,所以它不知道該如何釋放這塊“特殊”的記憶體區域,那麼這個時候java允許在類中定義一個由finalize()方法。

  特殊的區域例如:  
  1)由於在分配記憶體的時候可能採用了類似 C語言的做法,而非JAVA的通常new做法。這種情況主要發生在native method中,比如native method呼叫了C/C++方法malloc()函式系列來分配儲存空間,但是除非呼叫free()函式,否則這些記憶體空間將不會得到釋放,那麼這個時候就可能造成記憶體洩漏。但是由於free()方法是在C/C++中的函式,所以finalize()中可以用本地方法來呼叫它。以釋放這些“特殊”的記憶體空間。  

   2)又或者開啟的檔案資源,這些資源不屬於垃圾回收器的回收範圍。

2)又或者開啟的檔案資源,這些資源不屬於垃圾回收器的回收範圍。
換言之,finalize()的主要用途是釋放一些其他做法開闢的記憶體空間,以及做一些清理工作。因為在JAVA中並沒有提夠像“析構”函式或者類似概念的函式,要做一些類似清理工作的時候,必須自己動手建立一個執行清理工作的普通方法,也就是override Object這個類中的finalize()方法。例如,假設某一個物件在建立過程中會將自己繪製到螢幕上,如果不是明確地從螢幕上將其擦出,它可能永遠都不會被清理。如果在finalize()加入某一種擦除功能,當GC工作時,finalize()得到了呼叫,影象就會被擦除。要是GC沒有發生,那麼這個影象就會被一直儲存下來。
一旦垃圾回收器準備好釋放物件佔用的儲存空間,首先會去呼叫finalize()方法進行一些必要的清理工作。只有到下一次再進行垃圾回收動作的時候,才會真正釋放這個物件所佔用的記憶體空間。
  在普通的清除工作中,為清除一個物件,那個物件的使用者必須在希望進行清除的地點呼叫一個清除方法。這與C++”解構函式”的概念稍有抵觸。在C++中,所有物件都會破壞(清除)。或者換句話說,所有物件都”應該”破壞。若將C++物件建立成一個本地物件,比如在堆疊中建立(在Java中是不可能的,Java都在堆中),那麼清除或破壞工作就會在”結束花括號”所代表的、建立這個物件的作用域的末尾進行。若物件是用new建立的(類似於Java),那麼當程式設計師呼叫C++的 delete命令時(Java沒有這個命令),就會呼叫相應的解構函式。若程式設計師忘記了,那麼永遠不會呼叫解構函式,我們最終得到的將是一個記憶體”漏洞”,另外還包括物件的其他部分永遠不會得到清除。
  相反,Java不允許我們建立本地(區域性)物件–無論如何都要使用new。但在Java中,沒有”delete”命令來釋放物件,因為垃圾回收器會幫助我們自動釋放儲存空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾回收機制,所以Java沒有解構函式。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對解構函式的需要,或者說不能消除對解構函式代表的那種機制的需要(原因見下一段。另外finalize()函式是在垃圾回收器準備釋放物件佔用的儲存空間的時候被呼叫的,絕對不能直接呼叫finalize(),所以應儘量避免用它)。若希望執行除釋放儲存空間之外的其他某種形式的清除工作,仍然必須呼叫Java中的一個方法。它等價於C++的解構函式,只是沒後者方便。
在C++中所有的物件運用delete()一定會被銷燬,而JAVA裡的物件並非總會被垃圾回收器回收。In another word, 1 物件可能不被垃圾回收,2 垃圾回收並不等於“析構”,3 垃圾回收只與記憶體有關。也就是說,並不是如果一個物件不再被使用,是不是要在finalize()中釋放這個物件中含有的其它物件呢?不是的。因為無論物件是如何建立的,垃圾回收器都會負責釋放那些物件佔有的記憶體。
  在這個例子中,一個新的物件被建立,由於它沒有使用,所以該物件迅速地變為不可達,程式編譯後,執行命令: java -verbosegc TestGC 後結果為:
  [Full GC 168K->97K(1984K), 0.0253873 secs]
  機器的環境為,Windows 2000 + JDK1.3.1,箭頭前後的資料168K和97K分別表示垃圾收集GC前後所有存活物件使用的記憶體容量,說明有168K-97K=71K的物件容量被回收,括號內的資料1984K為堆記憶體的總容量,收集所需要的時間是0.0253873秒(這個時間在每次執行的時候會有所不同)。
需要注意的是,呼叫System.gc()也僅僅是一個請求(建議)。JVM接受這個訊息後,並不是立即做垃圾回收,而只是對幾個垃圾回收演算法做了加權,使垃圾回收操作容易發生,或提早發生,或回收較多而已。
update
@11/18
一. jvm 記憶體 結構圖

這裡寫圖片描述

儲存器
VM記憶體結構由堆、棧、本地方法棧、方法區等部分組成,另外JVM分別對新生代和舊生代採用不同的垃圾回收機制。
二.如何確定某個物件是“垃圾”?
我們先了解一個最基本的問題:如果確定某個物件是“垃圾”?既然垃圾收集器的任務是回收垃圾物件所佔的空間供新的物件使用,那麼垃圾收集器如何確定某個物件是“垃圾”?—即通過什麼方法判斷一個物件可以被回收了。
  在java中是通過引用來和物件進行關聯的,也就是說如果要操作物件,必須通過引用來進行。那麼很顯然一個簡單的辦法就是通過引用計數來判斷一 個物件是否可以被回收。不失一般性,如果一個物件沒有任何引用與之關聯,則說明該物件基本不太可能在其他地方被使用到,那麼這個物件就成為可被回收的物件 了。這種方式成為引用計數法。
  這種方式的特點是實現簡單,而且效率較高,但是它無法解決迴圈引用的問題,因此在Java中並沒有採用這種方式(Python採用的是引用計數法)。看下面這段程式碼:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

class MyObject{
    public Object object = null;
}

 最後面兩句將object1和object2賦值為null,也就是說object1和object2指向的物件已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數都不為0,那麼垃圾收集器就永遠不會回收它們。
  為了解決這個問題,在Java中採取了 可達性分析法。該方法的基本思想是通過一系列的“GC Roots”物件作為起點進行搜尋,如果在“GC Roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的,不過要注意的是被判定為不可達的物件不一定就會成為可回收物件。被判定為不可達的物件要 成為可回收物件必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收物件的可能性,則基本上就真的成為可回收物件了。
  至於可達性分析法具體是如何操作的我暫時也沒有看得很明白,如果有哪位朋友比較清楚的話請不吝指教。
  下面來看個例子:

Object aobj = new Object ( ) ; 
Object bobj = new Object ( ) ; 
Object cobj = new Object ( ) ; 
aobj = bobj;
aobj = cobj;
cobj = null;
aobj = null

第幾行有可能會使得某個物件成為可回收物件?第7行的程式碼會導致有物件會成為可回收物件。至於為什麼留給讀者自己思考。
  再看一個例子:

String str = new String("hello");
SoftReference<String> sr = new SoftReference<String>(new String("java"));
WeakReference<String> wr = new WeakReference<String>(new String("world"));

這三句哪句會使得String物件成為可回收物件?

答:第2句和第3句,第2句在記憶體不足的情況下會將String物件判定為可回收物件,第3句無論什麼情況下String物件都會被判定為可回收物件。
  
  最後總結一下平常遇到的比較常見的將物件判定為可回收物件的情況:
  1)顯示地將某個引用賦值為null或者將已經指向某個物件的引用指向新的物件,比如下面的程式碼:

Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;

 2)區域性引用所指向的物件,比如下面這段程式碼:

 
void fun() {

.....
    for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }   
}

 迴圈每執行完一次,生成的Object物件都會成為可回收的物件。
  3)只有弱引用與其關聯的物件,比如:
WeakReference wr = new WeakReference(new String(“world”));
1

三.觸發主GC(Garbage Collector)的條件
  JVM進行次GC的頻率很高,但因為這種GC佔用時間極短,所以對系統產生的影響不大。更值得關注的是主GC的觸發條件,因為它對系統影響很明顯。總的來說,有兩個條件會觸發主GC:
  1)當應用程式空閒時,即沒有應用執行緒在執行時,GC會被呼叫。因為GC在優先順序最低的執行緒中進行,所以當應用忙時,GC執行緒就不會被呼叫,但以下條件除外。
  2)Java堆記憶體不足時,GC會被呼叫。當應用執行緒在執行,並在執行過程中建立新物件,若這時記憶體空間不足,JVM就會強制地呼叫GC執行緒,以便回收記憶體用於新的分配。若GC一次之後仍不能滿足記憶體分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。
  由於是否進行主GC由JVM根據系統環境決定,而系統環境在不斷的變化當中,所以主GC的執行具有不確定性,無法預計它何時必然出現,但可以確定的是對一個長期執行的應用來說,其主GC是反覆進行的。
四.減少GC開銷的措施
  根據上述GC的機制,程式的執行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現記憶體駐留等一系列負面影響。為了避免這些影響,基本的原則就是儘可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:
  (1)不要顯式呼叫System.gc()
  此函式建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。
  (2)儘量減少臨時物件的使用
  臨時物件在跳出函式呼叫後,會成為垃圾,少用臨時變數就相當於減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。
  (3)物件不用時最好顯式置為Null
  一般而言,為Null的物件都會被作為垃圾處理,所以將不用的物件顯式地設為Null,有利於GC收集器判定垃圾,從而提高了GC的效率。
  (4)儘量使用StringBuffer,而不用String來累加字串
  由於String是固定長的字串物件,累加String物件時,並非在一個String物件中擴增,而是重新建立新的String物件,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾物件,因為對次作“+”操作時都必須建立新的String物件,但這些過渡物件對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間物件。
  (5)能用基本型別如Int,Long,就不用Integer,Long物件
  基本型別變數佔用的記憶體資源比相應物件佔用的少得多,如果沒有必要,最好使用基本變數。
  (6)儘量少用靜態物件變數
  靜態變數屬於全域性變數,不會被GC回收,它們會一直佔用記憶體。
  (7)分散物件建立或刪除的時間
  集中在短時間內大量建立新物件,特別是大物件,會導致突然需要大量記憶體,JVM在面臨這種情況時,只能進行主GC,以回收記憶體或整合記憶體碎片,從而增加主GC的頻率。集中刪除物件,道理也是一樣的。它使得突然出現了大量的垃圾物件,空閒空間必然減少,從而大大增加了下一次建立新物件時強制主GC的機會。
  
五. 關於垃圾回收的幾點補充
  經過上述的說明,可以發現垃圾回收有以下的幾個特點:3
  (1)垃圾收集發生的不可預知性:由於實現了不同的垃圾回收演算法和採用了不同的收集機制,所以它有可能是定時發生,有可能是當出現系統空閒CPU資源時發生,也有可能是和原始的垃圾收集一樣,等到記憶體消耗出現極限時發生,這與垃圾收集器的選擇和具體的設定都有關係。
  
  (2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標記活著的物件;(b)垃圾收集器能夠精確地定位物件之間的引用關係。前者是完全地回收所有廢棄物件的前提,否則就可能造成記憶體洩漏。而後者則是實現歸併和複製等演算法的必要條件。所有不可達物件都能夠可靠地得到回收,所有物件都能夠重新分配,允許物件的複製和物件記憶體的縮並,這樣就有效地防止記憶體的支離破碎。
  
  (3)現在有許多種不同的垃圾收集器,每種有其演算法且其表現各異,既有當垃圾收集開始時就停止應用程式的執行,又有當垃圾收集開始時也允許應用程式的執行緒執行,還有在同一時間垃圾收集多執行緒執行。
  
  (4)垃圾收集的實現和具體的JVM 以及JVM的記憶體模型有非常緊密的關係。不同的JVM 可能採用不同的垃圾收集,而JVM 的記憶體模型決定著該JVM可以採用哪些型別垃圾收集。現在,HotSpot 系列JVM中的記憶體系統都採用先進的面向物件的框架設計,這使得該系列JVM都可以採用最先進的垃圾收集。
  
  (5)隨著技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設定不同的引數,這就使得根據不同的應用環境獲得最優的應用效能成為可能。
  
針對以上特點,我們在使用的時候要注意:
  (1)不要試圖去假定垃圾收集發生的時間,這一切都是未知的。比如,方法中的一個臨時物件在方法呼叫完畢後就變成了無用物件,這個時候它的記憶體就可以被釋放。
  
  (2)Java中提供了一些和垃圾收集打交道的類,而且提供了一種強行執行垃圾收集的方法–呼叫System.gc(),但這同樣是個不確定的方法。Java 中並不保證每次呼叫該方法就一定能夠啟動垃圾收集,它只不過會向JVM發出這樣一個申請,到底是否真正執行垃圾收集,一切都是個未知數。
  
  (3)挑選適合自己的垃圾收集器。一般來說,如果系統沒有特殊和苛刻的效能要求,可以採用JVM的預設選項。否則可以考慮使用有針對性的垃圾收集器,比如增量收集器就比較適合實時性要求較高的系統之中。系統具有較高的配置,有比較多的閒置資源,可以考慮使用並行標記/清除收集器。
  
  (4)關鍵的也是難把握的問題是記憶體洩漏。良好的程式設計習慣和嚴謹的程式設計態度永遠是最重要的,不要讓自己的一個小錯誤導致記憶體出現大漏洞。
  
  (5)儘早釋放無用物件的引用。大多數程式設計師在使用臨時變數的時候,都是讓引用變數在退出活動域(scope)後,自動設定為null,暗示垃圾收集器來收集該物件,還必須注意該引用的物件是否被監聽,如果有,則要去掉監聽器,然後再賦空值。
  
六.垃圾回收機制的意義
在C++中,物件所佔的記憶體在程式結束執行之前一直被佔用,在明確釋放之前不能分配給其它物件;而在Java中,當沒有物件引用指向原先分配給某個物件的記憶體時,該記憶體便成為垃圾。JVM的一個系統級執行緒會自動釋放該記憶體塊。垃圾回收意味著程式不再需要的物件是”無用資訊”,這些資訊將被丟棄。當一個物件不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新物件使用。事實上,除了釋放沒用的物件,垃圾回收也可以清除記憶體記錄碎片。由於建立物件和垃圾回收器釋放丟棄物件所佔的記憶體空間,記憶體會出現碎片。碎片是分配給物件的記憶體塊之間的空閒記憶體洞。碎片整理將所佔用的堆記憶體移到堆的一端,JVM將整理出的記憶體分配給新的物件。
  垃圾回收能自動釋放記憶體空間,減輕程式設計的負擔。這使Java 虛擬機器具有一些優點。首先,它能使程式設計效率提高。在沒有垃圾回收機制的時候,可能要花許多時間來解決一個難懂的儲存器問題。在用Java語言程式設計的時候,靠垃圾回收機制可大大縮短時間。其次是它保護程式的完整性, 垃圾回收是Java語言安全性策略的一個重要部份。
  垃圾回收的一個潛在的缺點是它的開銷影響程式效能。Java虛擬機器必須追蹤執行程式中有用的物件,而且最終釋放沒用的物件。這一個過程需要花費處理器的時間。其次垃圾回收演算法的不完備性,早先採用的某些垃圾回收演算法就不能保證100%收集到所有的廢棄記憶體。當然隨著垃圾回收演算法的不斷改進以及軟硬體執行效率的不斷提升,這些問題都可以迎刃而解。