Java 垃圾收集機(jī)制

2018-09-28 19:22 更新

Java 垃圾收集機(jī)制

對(duì)象引用

Java 中的垃圾回收一般是在 Java 堆中進(jìn)行,因?yàn)槎阎袔缀醮娣帕?Java 中所有的對(duì)象實(shí)例。談到 Java 堆中的垃圾回收,自然要談到引用。在 JDK1.2 之前,Java 中的引用定義很很純粹:如果 reference 類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。但在 JDK1.2 之后,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將其分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強(qiáng)度依次減弱。

  • 強(qiáng)引用:如“Object obj = new Object()”,這類引用是 Java 程序中最普遍的。只要強(qiáng)引用還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
  • 軟引用:它用來描述一些可能還有用,但并非必須的對(duì)象。在系統(tǒng)內(nèi)存不夠用時(shí),這類引用關(guān)聯(lián)的對(duì)象將被垃圾收集器回收。JDK1.2 之后提供了 SoftReference 類來實(shí)現(xiàn)軟引用。
  • 弱引用:它也是用來描述非需對(duì)象的,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對(duì)象只能生存島下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在 JDK1.2 之后,提供了 WeakReference 類來實(shí)現(xiàn)弱引用。
  • 虛引用:最弱的一種引用關(guān)系,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。JDK1.2 之后提供了 PhantomReference 類來實(shí)現(xiàn)虛引用。

垃圾對(duì)象的判定

Java 堆中存放著幾乎所有的對(duì)象實(shí)例,垃圾收集器對(duì)堆中的對(duì)象進(jìn)行回收前,要先確定這些對(duì)象是否還有用,判定對(duì)象是否為垃圾對(duì)象有如下算法:

引用計(jì)數(shù)算法

給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加 1,當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1,任何時(shí)刻計(jì)數(shù)器都為 0 的對(duì)象就是不可能再被使用的。

引用計(jì)數(shù)算法的實(shí)現(xiàn)簡單,判定效率也很高,在大部分情況下它都是一個(gè)不錯(cuò)的選擇,當(dāng) Java 語言并沒有選擇這種算法來進(jìn)行垃圾回收,主要原因是它很難解決對(duì)象之間的相互循環(huán)引用問題。

根搜索算法

Java 和 C# 中都是采用根搜索算法來判定對(duì)象是否存活的。這種算法的基本思路是通過一系列名為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí),就證明此對(duì)象是不可用的。在 Java 語言里,可作為 GC Roots 的兌現(xiàn)包括下面幾種:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
  • 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。
  • 方法區(qū)中的常量引用的對(duì)象。
  • 本地方法棧中 JNI(Native 方法)的引用對(duì)象。

實(shí)際上,在根搜索算法中,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize()方法。當(dāng)對(duì)象沒有覆蓋 finalize()方法,或 finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為沒有必要執(zhí)行。如果該對(duì)象被判定為有必要執(zhí)行 finalize()方法,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為 F-Queue 隊(duì)列中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行 finalize()方法。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對(duì)象的 finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次),稍后 GC 將對(duì) F-Queue 中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中讓該對(duì)象重引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可。而如果對(duì)象這時(shí)還沒有關(guān)聯(lián)到任何鏈上的引用,那它就會(huì)被回收掉。

垃圾收集算法

判定除了垃圾對(duì)象之后,便可以進(jìn)行垃圾回收了。下面介紹一些垃圾收集算法,由于垃圾收集算法的實(shí)現(xiàn)涉及大量的程序細(xì)節(jié),因此這里主要是闡明各算法的實(shí)現(xiàn)思想,而不去細(xì)論算法的具體實(shí)現(xiàn)。

標(biāo)記—清除算法

標(biāo)記—清除算法是最基礎(chǔ)的收集算法,它分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,它的標(biāo)記過程其實(shí)就是前面的根搜索算法中判定垃圾對(duì)象的標(biāo)記過程。標(biāo)記—清除算法的執(zhí)行情況如下圖所示:

回收前狀態(tài):

回收后狀態(tài):

標(biāo)記—整理算法

復(fù)制算法比較適合于新生代,在老年代中,對(duì)象存活率比較高,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低,所以老年代一般會(huì)選用其他算法,如標(biāo)記—整理算法。該算法標(biāo)記的過程與標(biāo)記—清除算法中的標(biāo)記過程一樣,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同,它不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。標(biāo)記—整理算法的回收情況如下所示:

回收前狀態(tài):

回收后狀態(tài):

分代收集

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集 都采用分代收集,它根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊,一般是把 Java 堆分為新生代和老年代。在新生代中,每次垃圾收集時(shí)都會(huì)發(fā)現(xiàn)有大量對(duì)象死去,只有少量存活,因此可選用復(fù)制算法來完成收集,而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記—清除算法或標(biāo)記—整理算法來進(jìn)行回收。

垃圾收集器

垃圾收集器是內(nèi)存回收算法的具體實(shí)現(xiàn),Java 虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒有任何規(guī)定,因此不同廠商、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大的差別。Sun HotSpot 虛擬機(jī) 1.6 版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。這些收集器以不同的組合形式配合工作來完成不同分代區(qū)的垃圾收集工作。

垃圾回收分析

在用代碼分析之前,我們對(duì)內(nèi)存的分配策略明確以下三點(diǎn):

  • 對(duì)象優(yōu)先在 Eden 分配。
  • 大對(duì)象直接進(jìn)入老年代。
  • 長期存活的對(duì)象將進(jìn)入老年代。

對(duì)垃圾回收策略說明以下兩點(diǎn):

  • 新生代 GC(Minor GC):發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?Java 對(duì)象大多都具有朝生夕滅的特性,因此Minor GC 非常頻繁,一般回收速度也比較快。
  • 老年代 GC(Major GC/Full GC):發(fā)生在老年代的 GC,出現(xiàn)了 Major GC,經(jīng)常會(huì)伴隨至少一次 Minor GC。由于老年代中的對(duì)象生命周期比較長,因此 Major GC 并不頻繁,一般都是等待老年代滿了后才進(jìn)行 Full GC,而且其速度一般會(huì)比 Minor GC 慢 10 倍以上。另外,如果分配了 Direct Memory,在老年代中進(jìn)行 Full GC時(shí),會(huì)順便清理掉 Direct Memory 中的廢棄對(duì)象。

下面我們來看如下代碼:

public class SlotGc{  
    public static void main(String[] args){  
        byte[] holder = new byte[32*1024*1024];  
        System.gc();  
    }  
}  

代碼很簡單,就是向內(nèi)存中填充了 32MB 的數(shù)據(jù),然后通過虛擬機(jī)進(jìn)行垃圾收集。在 javac 編譯后,我們執(zhí)行如下指令:java -verbose:gc SlotGc 來查看垃圾收集的結(jié)果,得到如下輸出信息:

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

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

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

注意第三行,“->”之前的數(shù)據(jù)表示垃圾回收前堆中存活對(duì)象所占用的內(nèi)存大小,“->”之后的數(shù)據(jù)表示垃圾回收堆中存活對(duì)象所占用的內(nèi)存大小,括號(hào)中的數(shù)據(jù)表示堆內(nèi)存的總?cè)萘浚?.0094149 sec 表示垃圾回收所用的時(shí)間。

從結(jié)果中可以看出,System.gc()運(yùn)行后并沒有回收掉這 32MB 的內(nèi)存,這應(yīng)該是意料之中的結(jié)果,因?yàn)樽兞縣older 還處在作用域內(nèi),虛擬機(jī)自然不會(huì)回收掉 holder 引用的對(duì)象所占用的內(nèi)存。

我們把代碼修改如下:

public class SlotGc{  
    public static void main(String[] args){  
        {  
        byte[] holder = new byte[32*1024*1024];  
        }  
        System.gc();  
    }  
}  

加入花括號(hào)后,holder 的作用域被限制在了花括號(hào)之內(nèi),因此,在執(zhí)行System.gc()時(shí),holder 引用已經(jīng)不能再被訪問,邏輯上來講,這次應(yīng)該會(huì)回收掉 holder 引用的對(duì)象所占的內(nèi)存。但查看垃圾回收情況時(shí),輸出信息如下:

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

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

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

很明顯,這 32MB 的數(shù)據(jù)并沒有被回收。下面我們再做如下修改:

public class SlotGc{  
    public static void main(String[] args){  
        {  
        byte[] holder = new byte[32*1024*1024];  
        holder = null;  
        }  
        System.gc();  
    }  
}  

這次得到的垃圾回收信息如下:

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

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

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

說明這次 holder 引用的對(duì)象所占的內(nèi)存被回收了。我們慢慢來分析。

首先明確一點(diǎn):holder 能否被回收的根本原因是局部變量表中的 Slot 是否還存有關(guān)于 holder 數(shù)組對(duì)象的引用。

在第一次修改中,雖然在 holder 作用域之外進(jìn)行回收,但是在此之后,沒有對(duì)局部變量表的讀寫操作,holder 所占用的 Slot 還沒有被其他變量所復(fù)用(回憶 Java 內(nèi)存區(qū)域與內(nèi)存溢出一文中關(guān)于 Slot 的講解),所以作為 GC Roots 一部分的局部變量表仍保持者對(duì)它的關(guān)聯(lián)。這種關(guān)聯(lián)沒有被及時(shí)打斷,因此 GC 收集器不會(huì)將 holder 引用的對(duì)象內(nèi)存回收掉。 在第二次修改中,在 GC 收集器工作前,手動(dòng)將 holder 設(shè)置為 null 值,就把 holder 所占用的局部變量表中的 Slot 清空了,因此,這次 GC 收集器工作時(shí)將 holder 之前引用的對(duì)象內(nèi)存回收掉了。

當(dāng)然,我們也可以用其他方法來將 holder 引用的對(duì)象內(nèi)存回收掉,只要復(fù)用 holder 所占用的 slot 即可,比如在 holder 作用域之外執(zhí)行一次讀寫操作。

為對(duì)象賦 null 值并不是控制變量回收的最好方法,以恰當(dāng)?shù)淖兞孔饔糜騺砜刂谱兞炕厥諘r(shí)間才是最優(yōu)雅的解決辦法。另外,賦 null 值的操作在經(jīng)過虛擬機(jī) JIT 編譯器優(yōu)化后會(huì)被消除掉,經(jīng)過 JIT 編譯后,System.gc()執(zhí)行時(shí)就可以正確地回收掉內(nèi)存,而無需賦 null 值。

性能調(diào)優(yōu)

Java 虛擬機(jī)的內(nèi)存管理與垃圾收集是虛擬機(jī)結(jié)構(gòu)體系中最重要的組成部分,對(duì)程序(尤其服務(wù)器端)的性能和穩(wěn)定性有著非常重要的影響。性能調(diào)優(yōu)需要具體情況具體分析,而且實(shí)際分析時(shí)可能需要考慮的方面很多,這里僅就一些簡單常用的情況作簡要介紹。

  • 我們可以通過給 Java 虛擬機(jī)分配超大堆(前提是物理機(jī)的內(nèi)存足夠大)來提升服務(wù)器的響應(yīng)速度,但分配超大堆的前提是有把握把應(yīng)用程序的 Full GC 頻率控制得足夠低,因?yàn)橐淮?Full GC 的時(shí)間造成比較長時(shí)間的停頓。控制 Full GC 頻率的關(guān)鍵是保證應(yīng)用中絕大多數(shù)對(duì)象的生存周期不應(yīng)太長,尤其不能產(chǎn)生批量的、生命周期長的大對(duì)象,這樣才能保證老年代的穩(wěn)定。
  • Direct Memory 在堆內(nèi)存外分配,而且二者均受限于物理機(jī)內(nèi)存,且成負(fù)相關(guān)關(guān)系,因此分配超大堆時(shí),如果用到了 NIO 機(jī)制分配使用了很多的 Direct Memory,則有可能導(dǎo)致 Direct Memory 的 OutOfMemoryError 異常,這時(shí)可以通過 -XX:MaxDirectMemorySize 參數(shù)調(diào)整 Direct Memory 的大小。
  • 除了 Java 堆和永久代以及直接內(nèi)存外,還要注意下面這些區(qū)域也會(huì)占用較多的內(nèi)存,這些內(nèi)存的總和會(huì)受到操作系統(tǒng)進(jìn)程最大內(nèi)存的限制:

1、線程堆棧:可通過 -Xss 調(diào)整大小,內(nèi)存不足時(shí)拋出 StackOverflowError(縱向無法分配,即無法分配新的棧幀)或 OutOfMemoryError(橫向無法分配,即無法建立新的線程)。

2、Socket 緩沖區(qū):每個(gè) Socket 連接都有 Receive 和 Send 兩個(gè)緩沖區(qū),分別占用大約 37KB 和 25KB 的內(nèi)存。如果無法分配,可能會(huì)拋出 IOException:Too many open files 異常。關(guān)于 Socket 緩沖區(qū)的詳細(xì)介紹參見我的 Java 網(wǎng)絡(luò)編程系列中深入剖析 Socket 的幾篇文章。

3、JNI 代碼:如果代碼中使用了JNI調(diào)用本地庫,那本地庫使用的內(nèi)存也不在堆中。

4、虛擬機(jī)和 GC:虛擬機(jī)和 GC 的代碼執(zhí)行也要消耗一定的內(nèi)存。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)