JVM實戰(zhàn)---垃圾收集算法.md

Java會對內(nèi)存進行自動分配與回收管理啄寡,使上層業(yè)務更加安全割卖,方便地使用內(nèi)存實現(xiàn)程序邏輯
在不同的JVM實現(xiàn)及不同的回收機制中绷旗,堆內(nèi)存的劃分方式是不一樣的
這里簡要介紹垃圾回收( Garbage Collection, GC)灾部。垃圾回收的主要目的是清除不再使用的對象,自動釋放內(nèi)存.

靜態(tài)內(nèi)存分配和回收

在程序開始運行時由編譯器分配的內(nèi)存
在被編譯時就已經(jīng)能夠確定需要的空間,當程序被加載時系統(tǒng)把內(nèi)存一次性分配給它,這些內(nèi)存不會在程序執(zhí)行時發(fā)生變化,直到程序執(zhí)行結束時才回收內(nèi)存.

  • 包括原生數(shù)據(jù)類型及對象的引用

  • 這些靜態(tài)內(nèi)存空間在棧上分配,方法運行結束,對應的棧幀撤銷,內(nèi)存空間被回收.

  • 每個棧幀中的本地變量表都是在類被加載的時候就確定的,每一個棧幀中分配多少內(nèi)存基本上是在類結構確定時就已知了,因此這幾塊區(qū)域內(nèi)存分配和回收都具備確定性,就不需要過多考慮回收問題了

動態(tài)內(nèi)存分配和回收

  • 在程序執(zhí)行時才知道要分配的存儲空間大小,對象何時被回收也是不確定的,只有等到該對象不再使用才會被回收.

堆和方法區(qū)的內(nèi)存回收具有不確定性,因此垃圾收集器在回收堆和方法區(qū)內(nèi)存的時候花了一點心思.

1 Java堆內(nèi)存的回收

1.1 判定回收的對象

GC是如何判斷對象是否可以被回收的呢?為了判斷對象是否存活棒卷,JVM引入了GC Roots
如果一個對象與GC Roots之間沒有直接或間接的引用關系,比如某個失去任何引用的對象祝钢,或者兩個互相環(huán)島狀循環(huán)引用的對象等比规,判決這些對象“死緩”,是可以被回收的

在對堆進行對象回收之前,首先要判斷哪些是無效對象(一個對象不被任何對象或變量引用)需要被回收

一般有兩種判別方式:

  • 引用計數(shù)法 (Reference Counting)
    每個對象都有一個整型的計數(shù)器,當這個對象被一個變量或對象引用時拦英,該計數(shù)器加一;當該引用失效時,計數(shù)器值減一.當計數(shù)器為0時,就認為該對象是無效對象.
  • 可達性分析法 (Reachability Analysis)
    所有和GC Roots直接或間接關聯(lián)的對象都是有效對象,和GC Roots沒有關聯(lián)的對象就是無效對象.

    GC Roots對象
  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區(qū)中靜態(tài)屬性引用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧JNI(native方法)引用的對象

GC Roots并不包括堆中對象所引用的對象蜒什!這樣就不會出現(xiàn)循環(huán)引用.

  • 兩者對比
    引用計數(shù)法雖然簡單,但存在無法解決對象之間相互循環(huán)引用的嚴重問題,且伴隨加減法操作的性能影響.

因此,目前主流語言均使用可達性分析方法來判斷對象是否有效.

2 回收無效對象的過程

當經(jīng)可達性算法篩選出失效的對象之后,并不是立即清除,而是再給對象一次重生的機會

  • 判斷是否覆蓋finalize()
    • 未覆蓋該或已調用過該方法,直接釋放對象內(nèi)存
    • 已覆蓋該方法且還未被執(zhí)行,則將finalize()扔到F-Queue隊列中
  • 執(zhí)行F-Queue中的finalize()
    虛擬機會以較低的優(yōu)先級執(zhí)行這些finalize(),不會確保所有的finalize()都會執(zhí)行結束
    如果finalize()中出現(xiàn)耗時操作,虛擬機就直接停止執(zhí)行,將該對象清除
  • 對象重生或死亡
    • 如果在執(zhí)行finalize()方法時,將this賦給了某一個引用,則該對象重生
    • 如果沒有,那么就會被垃圾收集器清除

注意:強烈不建議使用finalize()進行任何操作!
如果需要釋放資源,請用try-finally或者其他方式都能做得更好.
因為finalize()不確定性大,開銷大,無法保證各個對象的調用順序.

以下代碼示例看到:一個對象的finalize被執(zhí)行,但依然可以存活

/**
 * 演示兩點:
 * 1.對象可以在被GC時自救
 * 2.這種自救機會只有一次,因為一個對象的finalize()最多只能被系統(tǒng)自動調用一次,因此第二次自救失敗
 * @author sss
 * @since 17-9-17 下午12:02
 *
 */
public class FinalizeEscapeGC {

    private static FinalizeEscapeGC SAVE_HOOK = null;

    private void isAlive() {
        System.out.println("yes,I am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize methodd executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }


    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        // 對象第一次成功自救
        SAVE_HOOK = null;
        System.gc();
        // 因為finalize方法優(yōu)先級很低,所以暫停0.5s以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }

        // 自救失敗
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }
    }
}

運行結果

finalize methodd executed!
yes,I am still alive :)
no,I am dead :(

3 方法區(qū)的內(nèi)存回收

使用復制算法實現(xiàn)堆的內(nèi)存回收,堆被分為新生代老年代

  • 新生代中的對象"朝生夕死",每次垃圾回收都會清除掉大量對象
  • 老年代中的對象生命較長,每次垃圾回收只有少量的對象被清除

由于方法區(qū)中存放生命周期較長的類信息、常量疤估、靜態(tài)變量.
因此方法區(qū)就像堆的老年代,每次GC只有少量垃圾被清除.

方法區(qū)中主要清除兩種垃圾

  • 廢棄常量
  • 無用類

3.1 回收廢棄常量

回收廢棄常量和回收對象類似,只要常量池中的常量不被任何變量或對象引用,那么這些常量就會被清除.

3.2 回收無用類

判定無用類的條件則較為苛刻

  • 該類所有實例都已被回收
    即Java堆不存在該類的任何實例
  • 加載該類的ClassLoader已被回收
  • 該類的java.lang.Class對象沒有被任何對象或變量引用,無法通過反射訪問該類的方法
    只要一個類被虛擬機加載進方法區(qū),那么在堆中就會有一個代表該類的對象:java.lang.Class.這個對象在類被加載進方法區(qū)的時候創(chuàng)建,在方法區(qū)中該類被刪除時清除.

4 垃圾收集算法

4.1 標記-清除(Mark-Sweep)

最基礎的收集算法,后續(xù)算法也都是基于此并改進其不足而得.

該算法會從每個GC Roots出發(fā)灾常,依次標記有引用關系的對象,最后將沒有被標記的對象清除

不足

這種算法會帶來大量的空間碎片铃拇,導致需要分配一個較大連續(xù)空間時容易觸發(fā)FullGC,降低了空間利用率.


標記-清除算法

為了解決這個問題钞瀑,又提出了“標記-整理算法”,該算法類似計算機的磁盤整理慷荔,首先會從GC Roots出發(fā)標記存活的對象雕什,然后將存活對象整理到內(nèi)存空間的一端,形成連續(xù)的已使用空間显晶,最后把已使用空間之外的部分全部清理掉贷岸,這樣就不會產(chǎn)生空間碎片的問題

4.2 復制算法(Mark-Copy)

為了能夠并行地標記和整理將空間分為兩塊,每次只激活其中一塊,垃圾回收時只需把存活的對象復制到另一塊未激活空間上,將未激活空間標記為已激活,將已激活空間標記為未激活,然后清除原空間中的原對象

將內(nèi)存分成大小相等兩份,只將數(shù)據(jù)存儲在其中一塊上

  • 當需要回收時,首先標記廢棄數(shù)據(jù)
  • 然后將有用數(shù)據(jù)復制到另一塊內(nèi)存
  • 最后將第一塊內(nèi)存空間全部清除


    復制算法

4.2.1 分析

  • 這種算法避免了空間碎片,但內(nèi)存縮小了一半.
  • 每次都需將有用數(shù)據(jù)全部復制到另一片內(nèi)存,效率不高

4.2.2 解決空間利用率問題

堆內(nèi)存空間分為較大的Eden和兩塊較小的Survivor,每次只使用Eden和Survivor區(qū)的一塊。這種情形下的“ Mark-Copy"減少了內(nèi)存空間的浪費吧碾』丝“Mark-Copy”現(xiàn)作為主流的YGC算法進行新生代的垃圾回收。
在新生代中,由于大量對象都是"朝生夕死",也就是一次垃圾收集后只有少量對象存活
因此我們可以將內(nèi)存劃分成三塊

  • Eden倦春、Survior1户敬、Survior2
  • 內(nèi)存大小分別是8:1:1

分配內(nèi)存時,只使用Eden和一塊Survior1.

  • 當發(fā)現(xiàn)Eden+Survior1的內(nèi)存即將滿時,JVM會發(fā)起一次Minor GC,清除掉廢棄的對象,
  • 并將所有存活下來的對象復制到另一塊Survior2中.
  • 接下來就使用Survior2+Eden進行內(nèi)存分配

通過這種方式,只需要浪費10%的內(nèi)存空間即可實現(xiàn)帶有壓縮功能的垃圾收集方法,避免了內(nèi)存碎片的問題.

4.2.3 分配擔保

準備為一個對象分配內(nèi)存時,發(fā)現(xiàn)此時Eden+Survior中空閑的區(qū)域無法裝下該對象
就會觸發(fā)MinorGC(新生代 GC 算法),對該區(qū)域的廢棄對象進行回收.

但如果MinorGC過后只有少量對象被回收,仍然無法裝下新對象

  • 那么此時需要將Eden+Survior中的所有對象轉移到老年代中,然后再將新對象存入Eden區(qū).這個過程就是"分配擔保".

在發(fā)生 minor gc 前,虛擬機會檢測老年代最大可用連續(xù)空間是否大于新生代所有對象總空間
若成立睁本,minor gc 可確保安全
若不成立尿庐,JVM會查看 HandlePromotionFailure是否允許擔保失敗

  • 若允許
    那么會繼續(xù)檢測老年代最大可用的連續(xù)空間是否 > 歷次晉升到老年代對象的平均大小
    • 若大于
      則將嘗試進行一次 minor gc,盡管這次 minor gc 是有風險的
    • 若小于或 HandlePromotionFailure 設置不允許冒險
      改為進行一次 full gc (老年代GC)

4.3 標記-壓縮算法(Mark-Compact)

在回收前,標記過程仍與"標記-清除"一樣
但后續(xù)不是直接清理可回收對象,而是

  • 將所有存活對象移到一端
  • 直接清掉端邊界之外內(nèi)存


    標記-整理算法

分析

這是一種老年代垃圾收集算法.
老年代中對象一般壽命較長,每次垃圾回收會有大量對象存活
因此如果選用"復制"算法,每次需要較多的復制操作,效率低

而且,在新生代中使用"復制"算法
當 Eden+Survior 都裝不下某個對象時,可使用老年代內(nèi)存進行"分配擔保"

而如果在老年代使用該算法,那么在老年代中如果出現(xiàn) Eden+Survior 裝不下某個對象時,沒有其他區(qū)域給他作分配擔保

因此,老年代中一般使用"標記-壓縮"算法

4.4 分代收集算法(Generational Collection)

當前商業(yè)虛擬機都采用此算法.
根據(jù)對象存活周期的不同將Java堆劃分為老年代和新生代,根據(jù)各個年代的特點使用最佳的收集算法.

  • 老年代中對象存活率高,無額外空間對其分配擔保,必須使用"標記-清除"或"標記-壓縮"算法
  • 新生代中存放"朝生夕死"的對象,用復制算法,只需要付出少量存活對象的復制成本,就可完成收集

5 Java中引用的種類

Java中根據(jù)生命周期的長短,將引用分為4類

  • 強引用
    我們平時所使用的引用就是強引用
    類似A a = new A();
    即通過關鍵字new創(chuàng)建的對象所關聯(lián)的引用就是強引用
    只要強引用還存在,該對象永遠不會被回收
  • 軟引用
    一些還有用但并非必需的對象
    只有當堆即將發(fā)生OOM異常時,JVM才會回收軟引用所指向的對象.
    軟引用通過SoftReference類實現(xiàn)
    軟引用的生命周期比強引用短一些
  • 弱引用
    也是描述非必需對象,比軟引用更弱
    所關聯(lián)的對象只能存活到下一次GC發(fā)生前.
    只要垃圾收集器工作,無論內(nèi)存是否足夠,弱引用所關聯(lián)的對象都會被回收.
    弱引用通過WeakReference類實現(xiàn).
  • 虛引用
    也叫幽靈(幻影)引用,最弱的引用關系.
    它和沒有引用沒有區(qū)別,無法通過虛引用取得對象實例.
    設置虛引用唯一的作用就是在該對象被回收之前收到一條系統(tǒng)通知.
    虛引用通過PhantomReference類來實現(xiàn).
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呢堰,一起剝皮案震驚了整個濱河市抄瑟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枉疼,老刑警劉巖皮假,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞋拟,死亡現(xiàn)場離奇詭異,居然都是意外死亡惹资,警方通過查閱死者的電腦和手機贺纲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褪测,“玉大人猴誊,你說我怎么就攤上這事∥甏耄” “怎么了懈叹?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長分扎。 經(jīng)常有香客問我澄成,道長,這世上最難降的妖魔是什么笆包? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任环揽,我火速辦了婚禮,結果婚禮上庵佣,老公的妹妹穿的比我還像新娘歉胶。我一直安慰自己,他們只是感情好巴粪,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布通今。 她就那樣靜靜地躺著,像睡著了一般肛根。 火紅的嫁衣襯著肌膚如雪辫塌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天派哲,我揣著相機與錄音臼氨,去河邊找鬼。 笑死芭届,一個胖子當著我的面吹牛储矩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播褂乍,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼持隧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了逃片?” 一聲冷哼從身側響起屡拨,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呀狼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裂允,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年赠潦,在試婚紗的時候發(fā)現(xiàn)自己被綠了叫胖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片草冈。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡她奥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怎棱,到底是詐尸還是另有隱情哩俭,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布拳恋,位于F島的核電站凡资,受9級特大地震影響,放射性物質發(fā)生泄漏谬运。R本人自食惡果不足惜隙赁,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梆暖。 院中可真熱鬧伞访,春花似錦、人聲如沸轰驳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽级解。三九已至冒黑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勤哗,已是汗流浹背抡爹。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芒划,地道東北人冬竟。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像腊状,于是被迫代替她去往敵國和親诱咏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容