java中垃圾回收機制GC(詳解)

常用定義

java垃圾回收

    在空閒時間以不定時的方式進行垃圾回收,回收的是無任何引用的對象佔據的內存空間而不是對象本身

觸發主GC(Garbage Collector)的條件

    (1)當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。因爲GC在優先級最低的線程中進行,所以當應用忙時,GC線程就不會被調用,但以下條件除外。
    (2)Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。

內存泄露

    程序中動態分配內存給一些臨時對象,但是對象不會被GC所回收,它始終佔用內存。即被分配的對象可達但已無用。

內存溢出

    程序運行過程中無法申請到足夠的內存而導致的一種錯誤。

爲什麼要有垃圾回收機制

    Java語言建立了垃圾收集機制,用以跟蹤正在使用的對象和發現並回收不再使用(引用)的對象。該機制可以有效防範動態內存分配中可能發生的兩個危險:因內存垃圾過多而引發的內存耗盡,以及不恰當的內存釋放所造成的內存非法引用。最終達到自動釋放內存空間,減輕編程的負擔的目的。

垃圾回收算法

引用計數法(Reference Counting Collector)

    引用計數是垃圾收集的早期策略。在這種方法中,堆中每一個對象都有一個引用計數。當一個對象被創建了,並且指向該對象的引用被分配給一個變量,這個對象的引用計數被設置爲1。比如新建一個對象A a=new A();然後a被分配給另外一個變量b,也就是b=a;那麼對象a的引用計數+1。當任何其他變量被賦值爲對這個對象的引用時,計數加1。當一個對象的引用超過生存期或者被設置一個新的值時,對象的引用計數減1,比如令b=c,則a的引用計數-1。任何引用計數爲0的對象可以被當做垃圾收集。當一個對象被垃圾收集的時候,它引用的任何對象計數減1。在這種方法中,一個對象被垃圾收集後可能導致後續其他對象的垃圾收集行動。比如A a=new A();b=a;當b被垃圾回收以後,a的引用計數變爲0,這樣導致a也被垃圾回收。
優點
    引用計數收集器可以很快執行,交織在程序的運行之中。這個提醒對於程序不能被長時間打斷的實時環境很有利。
缺點
    引用計數無法檢測出循環(即兩個或者更多的對象互相引用)。循環的例子如,父對象有一個子對象的引用,子對象又反過來引用父對象。這樣對象用戶都不可能計數爲0,就算它們已經無法被執行程序的根對象觸及。還有一個壞處就是,每次引用計數的增加或者減少都帶來額外的開銷。

標記-清除算法(Mark-Sweep)

    此算法是爲了解決引用計數法帶來的不足問題。垃圾回收器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每個可達對象設置一個或多個位。當掃描結束時,未被標記的對象就是無法觸及的,從而可以被收集。

複製算法(Copying)

    此算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。算法每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不過出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。

標記-整理算法(Mark-Compact)

    此算法結合了“標記-清除”和“複製”兩個算法的優點。但又爲了解決賦值算法的缺陷,充分利用內存空間,提出了”標記-整理”算法。該算法標記階段和”標記-清除”一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。

分代收集算法(Generational Collection)

    分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。
    目前大部分垃圾收集器對於新生代都採取Copying算法,因爲新生代中每次垃圾回收都要回收大部分對象,也就是說需要複製的操作次數較少,但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另一塊Survivor空間中,然後清理掉Eden和剛纔使用過的Survivor空間。
    而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。
    注意,在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。

這裏寫圖片描述

三.典型的垃圾收集器

常用的垃圾回收器

目前的收集器主要有三種:串行收集器、並行收集器、併發收集器。

一. 串行收集器

    使用單線程處理所有垃圾回收工作,因爲無需多線程交互,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。可以使用-XX:+UseSerialGC打開。

二. 並行收集器

  1. 對年輕代進行並行垃圾回收,因此可以減少垃圾回收時間。一般在多線程多處理器機器上使用。使用-XX:+UseParallelGC.打開。並行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了增強–可以堆年老代進行並行收集。如果年老代不使用併發收集的話,是使用單線程進行垃圾回收,因此會制約擴展能力。使用-XX:+UseParallelOldGC打開。
  2. 使用-XX:ParallelGCThreads=設置並行垃圾回收的線程數。此值可以設置與機器處理器數量相等。
  3. 此收集器可以進行如下配置:
    • 最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,通過-XX:MaxGCPauseMillis=指定。爲毫秒.如果指定了此值的話,堆大小和垃圾回收相關參數會進行調整以達到指定值。設定此值可能會減少應用的吞吐量。
    • 吞吐量:吞吐量爲垃圾回收時間與非垃圾回收時間的比值,通過-XX:GCTimeRatio=來設定,公式爲1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。默認情況爲99,即1%的時間用於垃圾回收。

三. 併發收集器

    可以保證大部分工作都併發進行(應用不停止),垃圾回收只暫停很少的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC打開。
1. 併發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收線程,跟蹤可達對象。在每個年老代垃圾回收週期中,在收集初期併發收集器會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個線程同時進行垃圾回收工作。
2. 併發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。
3. 在只有一個處理器的主機上使用併發收集器,設置爲incremental mode模式也可獲得較短的停頓時間。
4. 浮動垃圾:由於在應用運行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收週期時才能回收掉。所以,併發收集器一般需要20%的預留空間用於這些浮動垃圾。
5. Concurrent Mode Failure:併發收集器在應用運行時進行收集,所以需要保證堆在垃圾回收的這段時間有足夠的空間供程序使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下將會發生“併發模式失敗”,此時整個應用將會暫停,進行垃圾回收。
6. 啓動併發收集器:因爲併發收集在應用運行時進行收集,所以必須保證收集完成之前有足夠的內存空間供程序使用,否則會出現“Concurrent Mode Failure”。通過設置-XX:CMSInitiatingOccupancyFraction=指定還有多少剩餘堆時開始執行併發收集

四. 小結

  • 串行處理器:
    –適用情況:數據量比較小(100M左右);單處理器下並且對響應時間無要求的應用。
    –缺點:只能用於小型應用
  • 並行處理器:
    –適用情況:“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:後臺處理、科學計算。
    –缺點:應用響應時間可能較長
  • 併發處理器:
    –適用情況:“對響應時間有高要求”,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web服務器/應用服務器、電信交換、集成開發環境。

如何影響java垃圾回收

    通常我們在開發中無法控制JVM的垃圾回收機制,但是可以通過編程的手段來影響垃圾回收,目的是讓對象符合垃圾回收條件。
1.將無用對象賦值爲null
2.重新爲引用變量賦值

例如:

 Person p = new Person("aaa");
 p = new Person("bbb");
  這樣,new Person(“aaa”)這個對象就是垃圾了—-符合垃圾回收條件了。
3.讓相互聯繫的對象稱爲“島”對象
 Person p1 = new Person("aaa");
 Person p2 = new Person("bbb");
 Person p3 = new Person("ccc");
 p1=p2; p2=p3; p3=p1;
 p1=null; p2=null; p3=null;
 在沒有對p1、p2、p3置null之前,它們之間是一種三角戀關係。分別置null,三角戀關係依然存在,但是三個變量不在使用它們了。三個Person對象就組成了一個孤島,最後死在堆上—-被垃圾回收掉。
4.強制的垃圾回收System.gc()
System.gc()
Runtime.getRuntime().gc() 
  • 1
  • 2

    上面的方法用於顯式的通知JVM可以進行一次垃圾回收,但真正垃圾回收機制具體在什麼時間點開始進行垃圾回收是不可預料的;唯一能保證的是當你內存在極少的情況,垃圾回收器在程序拋出OutofMemaryException之前運行一次。

finalize()方法

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

protected void finalize() throws Throwable
  • 1

finalize()方法的理解:
1.finalize()方法是Object中的方法。
2.finalize()方法會在對象被垃圾回收之前被垃圾回收器調用一次,這是Java語言的一種機制。
3.finalize()方法在任何對象上最多隻會被垃圾回收器調用一次。
    在finalize()方法返回之後,對象消失,垃圾收集開始執行。原型中的throws Throwable表示它可以拋出任何類型的異常。之所以要使用finalize(),是存在着垃圾回收器不能處理的特殊情況。假定你的對象(並非使用new方法)獲得了一塊“特殊”的內存區域,由於垃圾回收器只知道那些顯示地經由new分配的內存空間,所以它不知道該如何釋放這塊“特殊”的內存區域,那麼這個時候java允許在類中定義一個由finalize()方法。
finalize()方法使用陷阱:
1.垃圾回收器無法保證垃圾對象能被回收,因此,finalize()方法也無法保證運行。建議不要重寫finalize()方法,即使重寫,也不要在finalize()方法中寫關鍵的代碼。
2.finalize()方法中可以把自己傳遞個別的對象,這樣就不是垃圾了,避免了被回收。但是當下次這個對象又符合垃圾回收的時候,finalize()方法不會被調用第二次了,而是直接被清理掉了。

開發中常用的減少GC開銷的措施

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

    此函數建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。這裏特別需要說明的是,在代碼中顯示的調用System.gc(),並不一定能夠進行GC,這個我們可以通過finalize()方法進行驗證,即主動調用System.gc(),並不一定每次都調用finalize()方法。finalize()方法的特徵是在對象被回收之前, 首先調用finalize()方法。

(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對象

    基本類型變量佔用的內存資源比相應對象佔用的少得多,如果沒有必要,最好使用基本變量。什麼情況下需要使用Integer?

(6)儘量少用靜態對象變量

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

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

    集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回收內存或整合內存碎片, 從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾對象,空閒空間必然減少,從而大大增加了下一次創建新對象時強制主GC 的機會。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章