理解JVM之垃圾收集器概述
目錄
前言
很多人將垃圾收集(Garbage Collection)視為Java的伴生產(chǎn)物,實(shí)際1960年誕生的Lisp是第一門真正使用內(nèi)存動(dòng)態(tài)分配與垃圾收集技術(shù)的語(yǔ)言。在目前看來(lái),內(nèi)存的動(dòng)態(tài)分配與內(nèi)存回收已經(jīng)相當(dāng)成熟,但了解GC與內(nèi)存分配還是非常有必要的,當(dāng)排查內(nèi)存溢出、內(nèi)存泄漏問(wèn)題,當(dāng)垃圾收集稱為系統(tǒng)高并發(fā)的瓶頸時(shí),就需要我們對(duì)其實(shí)施必要的監(jiān)控與調(diào)節(jié)。
在前面的篇章中我們了解到Java的運(yùn)行時(shí)區(qū)域中的程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧的內(nèi)存分配與回收具有確定性,但Java堆不同,這部分內(nèi)存的分配與回收都是動(dòng)態(tài)的,因而垃圾收集器關(guān)注的就是這部分內(nèi)存。
一、對(duì)象的生與死
堆中幾乎存放著Java中的所有的對(duì)象實(shí)例,但在對(duì)堆進(jìn)行回收前,首先要確定對(duì)象的“生死”,下面就這方面進(jìn)行講解:
1、引用記數(shù)算法
很多書籍中判斷對(duì)象存活方法的判斷:給對(duì)象中添加引用計(jì)數(shù)器,每當(dāng)一個(gè)地方引用它,計(jì)數(shù)器值加1;當(dāng)引用失效,計(jì)數(shù)器值減1;任何時(shí)刻都為0的對(duì)象時(shí)不能再使用的。引用計(jì)數(shù)算法應(yīng)用于python,FlashPlayer及Squirrel中,但Java卻沒(méi)有選用引用計(jì)數(shù)器算法管理內(nèi)存,最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
2、根搜索算法
主流的商用程序語(yǔ)言(Java、C#、Lisp)都使用根搜索算法(GC Roots Tracing)判定對(duì)象是否存活,又稱為可達(dá)性分析算法。算法的基本思路是通過(guò)一系列名為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象GC Roots沒(méi)有任何引用鏈相連(用圖論的表示從GC Roots都這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象不可用。如下圖所示,object4,object5,object6相互關(guān)聯(lián),但到GC Roots不可達(dá),因而可判定為可回收對(duì)象。

Java中,可作為GC Roots的對(duì)象一般包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象
- 方法區(qū)中的類靜態(tài)屬性的引用對(duì)象
- 方法區(qū)中的常量引用的對(duì)象
- 本地方法棧中的JNI(Native方法)的引用對(duì)象。
3、引用的理解
無(wú)論時(shí)引用計(jì)數(shù)算法還是根搜索算法,判斷對(duì)象存活都與引用相關(guān)。JDK1.2之前對(duì)Java的引用定義:如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表另一塊內(nèi)存的起始地址,稱這塊內(nèi)存代表一個(gè)引用。這種定義過(guò)于狹隘,一個(gè)對(duì)象這種定義下只有引用或沒(méi)被引用兩種狀態(tài),這再很多應(yīng)用場(chǎng)景下是無(wú)法描述的。JDK1.2之后,Java對(duì)應(yīng)用概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、需引用(Phantom Reference)四種,且強(qiáng)度依此減弱。
- 強(qiáng)引用
強(qiáng)引用是指程序代碼之中普遍存在的,類似“Object obj = new Object()”的引用,只要強(qiáng)引用存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。 - 軟引用
軟引用用來(lái)描述一些非必需的有用對(duì)象。在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,會(huì)把軟引用關(guān)聯(lián)的對(duì)象列進(jìn)回收范圍之中并進(jìn)行第二次回收。若此次回收沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。 - 弱應(yīng)用
弱引用描述非必需對(duì)象,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生前,當(dāng)垃圾回收時(shí),無(wú)論內(nèi)存是否足夠,都會(huì)回收掉只被若引用關(guān)聯(lián)的對(duì)象。 - 虛引用
虛引用也稱為幽靈引用或幻影引用,一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用取得一個(gè)對(duì)象實(shí)例。對(duì)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望在這個(gè)對(duì)象被收集器收集回收時(shí)收到一個(gè)系統(tǒng)通知。JDK1.2之后,提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用。
4、生存死亡的判斷
在根搜索算法中不可達(dá)的對(duì)象,并非是“必死”的,暫時(shí)處于“緩刑”階段一個(gè)對(duì)象正式死亡,要經(jīng)理至少兩次標(biāo)記過(guò)程。首先如果在進(jìn)行根搜索后沒(méi)有發(fā)現(xiàn)與GC Roots相連的引用鏈,它將會(huì)被第一次標(biāo)記并進(jìn)行一次篩選,篩選的條件是該對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或finalize()方法已被虛擬機(jī)調(diào)用過(guò),則虛擬機(jī)認(rèn)定該對(duì)象為“沒(méi)有必要執(zhí)行”。若該對(duì)象被判定為有必要執(zhí)行finalize()方法,那對(duì)象將被放置在一個(gè)名為F-Quenue隊(duì)列中,并在稍后由一條虛擬機(jī)自動(dòng)建立的,低優(yōu)先級(jí)的Finalizer線程執(zhí)行。這里的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)finalize()方法,但并不承諾會(huì)等待方法運(yùn)行結(jié)束。
這是因?yàn)槿粼趫?zhí)行過(guò)程中出現(xiàn)某對(duì)象的finalize()方法執(zhí)行緩慢或發(fā)生死循環(huán),隊(duì)列中的其他對(duì)象將會(huì)永久處于等待狀態(tài),甚至導(dǎo)致內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對(duì)象逃脫死亡的最后一次機(jī)會(huì),稍后GC將對(duì)F-quene的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記,若要逃脫死亡命運(yùn),只需重新與引用鏈上的任意對(duì)象關(guān)聯(lián)即可,如此,在第二次標(biāo)記時(shí)它將被移出即將回收集合。還沒(méi)有逃脫的對(duì)象只能靜靜等待死亡的到來(lái)。
5、回收方法區(qū)
很多人認(rèn)為方法區(qū)(HotSpot中的永久代)是沒(méi)有垃圾收集的,Java虛擬機(jī)規(guī)范中不要求在方法區(qū)實(shí)現(xiàn)垃圾收集,且在方法區(qū)進(jìn)行垃圾收集的性能低。永久代的垃圾收集處理的主要是:廢棄常量和無(wú)用的類?;厥粘A颗cJava堆中對(duì)象相似。常量池中的回收是判斷是否由其他對(duì)象引用常量池中的常量,若沒(méi)有,若此時(shí)發(fā)生內(nèi)存回收,此常量將被清出常量池。而對(duì)于無(wú)用的類的判斷條件則相對(duì)苛刻,需滿足三個(gè)條件:
- 此類所有實(shí)例已被回收,即堆中不存在此類的任何實(shí)例
- 加載該類的ClassLoader已被回收
- 此類對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
虛擬機(jī)可堆滿足條件的無(wú)用類進(jìn)行回收,但具體是否回收,需虛擬機(jī)提供的參數(shù)-Xnoclassgc進(jìn)行控制,還可查看類的加載與卸載信息,在大量使用反射、動(dòng)態(tài)代理等場(chǎng)景及頻繁定義ClassLoader的場(chǎng)景需要具備類卸載功能,保證永久代不會(huì)溢出。
二、垃圾收集算法
下面就幾種算法的思想及發(fā)展過(guò)程進(jìn)行介紹。
1、標(biāo)記 — 清除算法
標(biāo)記清除算法(Mark-Sweep)算法是最基礎(chǔ)的收集算法,它分為“標(biāo)記”和“清除”兩部分:首先標(biāo)記出所有需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,他的標(biāo)記過(guò)程就是對(duì)象的標(biāo)記判定的過(guò)程。后續(xù)的收集算法都是基于這種思路并堆其缺點(diǎn)進(jìn)行改進(jìn)得到的。它的缺點(diǎn)有兩個(gè):效率,標(biāo)記與清除的過(guò)程效率都不高;空間,標(biāo)記清除后產(chǎn)生大量的不連續(xù)內(nèi)存碎片,空間碎片過(guò)多可能導(dǎo)致程序運(yùn)行分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存提前出發(fā)另一次垃圾收集。下圖為標(biāo)記清除算法示意圖:

2、復(fù)制算法
為解決效率問(wèn)題,出現(xiàn)復(fù)制收集算法(Copying),它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次使用其中一塊,當(dāng)一塊用完,將活著的對(duì)象復(fù)制到另一塊上,將已使用過(guò)的內(nèi)存空間一此清理掉。使得每次都是堆其中一塊進(jìn)行內(nèi)存回收,就可以忽略了內(nèi)存碎片的復(fù)雜情況,只需移動(dòng)堆頂指針,按順序分配內(nèi)存。算法的代價(jià)是將內(nèi)存縮小為原來(lái)的一半,算法執(zhí)行過(guò)程如圖所示。

當(dāng)前商業(yè)虛擬機(jī)采用此算法回收新生代,由于新生代的對(duì)象98%是朝生夕死,因而秩序?qū)?nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一塊Survior?;厥諘r(shí),將Eden和Survivor存活的對(duì)象一次拷貝到另一塊Survivor空間上,最后清理掉Eden和剛才使用過(guò)的Survivor空間。Eden和Survivor大小比例為8:1,每次新生代可用內(nèi)存空間為90%,但98%回收只是一般情況,若Survivor空間不夠用,需要依賴其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保,即通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。
3、標(biāo)記 — 整理算法
復(fù)制收集算法在對(duì)象存活率較高時(shí)就執(zhí)行較多的復(fù)制操作,效率將會(huì)變低,更關(guān)鍵的是,需要額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)所有對(duì)象都存活的極端情況,所以老年代一般不直接選用此算法。針對(duì)老年機(jī)的特點(diǎn),又提出了”標(biāo)記整理(Mark-Compact)“算法,標(biāo)記過(guò)程與”標(biāo)記-清除算法一致“,后續(xù)步驟是直接堆可回收的對(duì)象進(jìn)行清理,讓所有存活對(duì)象向一端移動(dòng),直接清理掉端界面以外的內(nèi)存,標(biāo)記-整理算法示意圖如圖所示:

4、分代收集算法
目前的商用虛擬機(jī)垃圾收集采用的是“分代收集(Generational Collection)”算法,它根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊,一般把Java堆分為新生代和老年代,可根據(jù)各個(gè)年代的特點(diǎn)采用最合適的收集算法。在新生代中,對(duì)象死亡多,存活少,采用復(fù)制算法,付出少量復(fù)制成本完成收集。老年代存活率高、沒(méi)有額外空間進(jìn)行分配擔(dān)保,使用”標(biāo)記-整理“或”標(biāo)記-清理“算法進(jìn)行回收。
本文主要參考自《深入理解Java虛擬機(jī)——JVM高級(jí)特性與最佳實(shí)踐》一書