【深刻Java虛擬機】之八:Java垃圾收集機制

對象引用

    Java中的垃圾回收通常是在Java堆中進行,由於堆中幾乎存放了Java中全部的對象實例。談到Java堆中的垃圾回收,天然要談到引用。在JDK1.2以前,Java中的引用定義很很純粹:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。但在JDK1.2以後,Java對引用的概念進行了擴充,將其分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。java

  • 強引用:如「Object obj = new Object()」,這類引用是Java程序中最廣泛的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用:它用來描述一些可能還有用,但並不是必須的對象。在系統內存不夠用時,這類引用關聯的對象將被垃圾收集器回收。JDK1.2以後提供了SoftReference類來實現軟引用。
  • 弱引用:它也是用來描述非需對象的,但它的強度比軟引用更弱些,被弱引用關聯的對象只能生存島下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2以後,提供了WeakReference類來實現弱引用。
  • 虛引用:最弱的一種引用關係,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的是但願能在這個對象被收集器回收時收到一個系統通知。JDK1.2以後提供了PhantomReference類來實現虛引用。

 

垃圾對象的斷定

    Java堆中存放着幾乎全部的對象實例,垃圾收集器對堆中的對象進行回收前,要先肯定這些對象是否還有用,斷定對象是否爲垃圾對象有以下算法:算法

    引用計數算法

    給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1,任什麼時候刻計數器都爲0的對象就是不可能再被使用的。編程

    引用計數算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的選擇,當Java語言並無選擇這種算法來進行垃圾回收,主要緣由是它很難解決對象之間的相互循環引用問題。數組

    根搜索算法

    Java和C#中都是採用根搜索算法來斷定對象是否存活的。這種算法的基本思路是經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證實此對象是不可用的。在Java語言裏,可做爲GC Roots的兌現包括下面幾種:服務器

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中的類靜態屬性引用的對象。
  • 方法區中的常量引用的對象。
  • 本地方法棧中JNINative方法)的引用對象。

    實際上,在根搜索算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GC Roots相鏈接的引用鏈,那它會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行。若是該對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue隊列中,並在稍後由一條由虛擬機自動創建的、低優先級的Finalizer線程去執行finalize()方法。finalize()方法是對象逃脫死亡命運的最後一次機會(由於一個對象的finalize()方法最多隻會被系統自動調用一次),稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是要在finalize()方法中成功拯救本身,只要在finalize()方法中讓該對象重引用鏈上的任何一個對象創建關聯便可。而若是對象這時尚未關聯到任何鏈上的引用,那它就會被回收掉。網絡

 

垃圾收集算法

    斷定除了垃圾對象以後,即可以進行垃圾回收了。下面介紹一些垃圾收集算法,因爲垃圾收集算法的實現涉及大量的程序細節,所以這裏主要是闡明各算法的實現思想,而不去細論算法的具體實現。ide

    標記—清除算法

    標記—清除算法是最基礎的收集算法,它分爲「標記」和「清除」兩個階段:首先標記出所需回收的對象,在標記完成後統一回收掉全部被標記的對象,它的標記過程其實就是前面的根搜索算法中斷定垃圾對象的標記過程。標記—清除算法的執行狀況以下圖所示:性能

    回收前狀態:優化

    回收後狀態:

 

 

    該算法有以下缺點:spa

  • 標記和清除過程的效率都不高。
  • 標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不觸發另外一次垃圾收集動做。

    複製算法

    複製算法是針對標記—清除算法的缺點,在其基礎上進行改進而獲得的,它講課用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活着的對象複製到另一塊內存上面,而後再把已使用過的內存空間一次清理掉。複製算法有以下優勢:

  • 每次只對一塊內存進行回收,運行高效。
  • 只需移動棧頂指針,按順序分配內存便可,實現簡單。
  • 內存回收時不用考慮內存碎片的出現。

    它的缺點是:可一次性分配的最大內存縮小了一半。

    複製算法的執行狀況以下圖所示:

    回收前狀態:

    回收後狀態:

    標記—整理算法

    複製算法比較適合於新生代,在老年代中,對象存活率比較高,若是執行較多的複製操做,效率將會變低,因此老年代通常會選用其餘算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程同樣,但對標記後出的垃圾對象的處理狀況有所不一樣,它不是直接對可回收對象進行清理,而是讓全部的對象都向一端移動,而後直接清理掉端邊界之外的內存。標記—整理算法的回收狀況以下所示:

    回收前狀態:

    回收後狀態:

 

    分代收集

    當前商業虛擬機的垃圾收集 都採用分代收集,它根據對象的存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少許存活,所以可選用複製算法來完成收集,而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。

 

垃圾收集器

    垃圾收集器是內存回收算法的具體實現,Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣廠商、不一樣版本的虛擬機所提供的垃圾收集器均可能會有很大的差異。Sun  HotSpot虛擬機1.6版包含了以下收集器:SerialParNewParallel ScavengeCMSSerial OldParallel Old。這些收集器以不一樣的組合形式配合工做來完成不一樣分代區的垃圾收集工做。

 

垃圾回收分析   

    在用代碼分析以前,咱們對內存的分配策略明確如下三點:
  • 對象優先在Eden分配。
  • 大對象直接進入老年代。
  • 長期存活的對象將進入老年代。
    對垃圾回收策略說明如下兩點:
  • 新生代GC(Minor GC):發生在新生代的垃圾收集動做,由於Java對象大多都具備朝生夕滅的特性,所以Minor GC很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC/Full GC):發生在老年代的GC,出現了Major GC,常常會伴隨至少一次Minor GC。因爲老年代中的對象生命週期比較長,所以Major GC並不頻繁,通常都是等待老年代滿了後才進行Full GC,並且其速度通常會比Minor GC慢10倍以上。另外,若是分配了Direct Memory,在老年代中進行Full GC時,會順便清理掉Direct Memory中的廢棄對象。

    下面咱們來看以下代碼:

1 public class SlotGc{
2     public static void main(String[] args){
3         byte[] holder = new byte[32*1024*1024];
4         System.gc();
5     }
6 }
View Code
    代碼很簡單,就是向內存中填充了32MB的數據,而後經過虛擬機進行垃圾收集。在Javac編譯後,咱們執行以下指令:java -verbose:gc SlotGc來查看垃圾收集的結果,獲得以下輸出信息:

    [GC 208K->134K(5056K), 0.0017306 secs]

    [Full GC 134K->134K(5056K), 0.0121194 secs]

    [Full GC 32902K->32902K(37828K), 0.0094149 sec

    注意第三行,「->」以前的數據表示垃圾回收前堆中存活對象所佔用的內存大小,「->」以後的數據表示垃圾回收堆中存活對象所佔用的內存大小,括號中的數據表示堆內存的總容量,0.0094149 sec 表示垃圾回收所用的時間。

    從結果中能夠看出,System.gc(()運行後並無回收掉這32MB的內存,這應該是意料之中的結果,由於變量holder還處在做用域內,虛擬機天然不會回收掉holder引用的對象所佔用的內存。

    咱們把代碼修改以下:

1 public class SlotGc{
2     public static void main(String[] args){
3         {
4         byte[] holder = new byte[32*1024*1024];
5         }
6         System.gc();
7     }
8 }
View Code

     加入花括號後,holder的做用域被限制在了花括號以內,所以,在執行System.gc()時,holder引用已經不能再被訪問,邏輯上來說,此次應該會回收掉holder引用的對象所佔的內存。但查看垃圾回收狀況時,輸出信息以下:

    [GC 208K->134K(5056K), 0.0017100 secs]

    [Full GC 134K->134K(5056K), 0.0125887 secs]

    [Full GC 32902K->32902K(37828K), 0.0089226 secs]

    很明顯,這32MB的數據並無被回收。下面咱們再作以下修改:

1 public class SlotGc{
2     public static void main(String[] args){
3         {
4         byte[] holder = new byte[32*1024*1024];
5         holder = null;
6         }
7         System.gc();
8     }
9 }
View Code

     此次獲得的垃圾回收信息以下:

    [GC 208K->134K(5056K), 0.0017194 secs]

    [Full GC 134K->134K(5056K), 0.0124656 secs]

    [Full GC 32902K->134K(37828K), 0.0091637 secs]

    說明此次holder引用的對象所佔的內存被回收了。咱們慢慢來分析。

    首先明確一點:holder可否被回收的根本緣由是局部變量表中的Slot是否還存有關於holder數組對象的引用。

在第一次修改中,雖然在holder做用域以外進行回收,可是在此以後,沒有對局部變量表的讀寫操做,holder所佔用的Slot尚未被其餘變量所複用(回憶Java內存區域與內存溢出一文中關於Slot的講解),因此做爲GC Roots一部分的局部變量表仍保持者對它的關聯。這種關聯沒有被及時打斷,所以GC收集器不會將holder引用的對象內存回收掉。 在第二次修改中,在GC收集器工做前,手動將holder設置爲null值,就把holder所佔用的局部變量表中的Slot清空了,所以,此次GC收集器工做時將holder以前引用的對象內存回收掉了。

    固然,咱們也能夠用其餘方法來將holder引用的對象內存回收掉,只要複用holder所佔用的slot便可,好比在holder做用域以外執行一次讀寫操做。

    爲對象賦null值並非控制變量回收的最好方法,以恰當的變量做用域來控制變量回收時間纔是最優雅的解決辦法。另外,賦null值的操做在通過虛擬機JIT編譯器優化後會被消除掉,通過JIT編譯後,System.gc()執行時就能夠正確地回收掉內存,而無需賦null值。

 

性能調優 

    Java虛擬機的內存管理與垃圾收集是虛擬機結構體系中最重要的組成部分,對程序(尤爲服務器端)的性能和穩定性有着很是重要的影響。性能調優須要具體狀況具體分析,並且實際分析時可能須要考慮的方面不少,這裏僅就一些簡單經常使用的狀況做簡要介紹。   

    • 咱們能夠經過給Java虛擬機分配超大堆(前提是物理機的內存足夠大)來提高服務器的響應速度,但分配超大堆的前提是有把握把應用程序的Full GC頻率控制得足夠低,由於一次Full GC的時間形成比較長時間的停頓。控制Full GC頻率的關鍵是保證應用中絕大多數對象的生存週期不該太長,尤爲不能產生批量的、生命週期長的大對象,這樣才能保證老年代的穩定。
    • Direct Memory在堆內存外分配,並且兩者均受限於物理機內存,且成負相關關係,所以分配超大堆時,若是用到了NIO機制分配使用了不少的Direct Memory,則有可能致使Direct Memory的OutOfMemoryError異常,這時能夠經過-XX:MaxDirectMemorySize參數調整Direct Memory的大小。
    • 除了Java堆和永久代以及直接內存外,還要注意下面這些區域也會佔用較多的內存,這些內存的總和會受到操做系統進程最大內存的限制:

      一、線程堆棧:可經過-Xss調整大小,內存不足時拋出StackOverflowError(縱向沒法分配,即沒法分配新的棧幀)或OutOfMemoryError(橫向沒法分配,即沒法創建新的線程)。

      二、Socket緩衝區:每一個Socket鏈接都有ReceiveSend兩個緩衝區,分別佔用大約37KB25KB的內存。若是沒法分配,可能會拋出IOExceptionToo many open files異常。關於Socket緩衝區的詳細介紹參見個人Java網絡編程系列中深刻剖析Socket的幾篇文章。

      三、JNI代碼:若是代碼中使用了JNI調用本地庫,那本地庫使用的內存也不在堆中。

      四、虛擬機和GC:虛擬機和GC的代碼執行也要消耗必定的內存。

        轉自:http://blog.csdn.net/ns_code/article/details/18076173
相關文章
相關標籤/搜索