我對JVM中垃圾回收機制的理解

前言

作為一名Java編程者资铡,想要往高級進階又沾,內(nèi)存管理往往是避不開的環(huán)節(jié)励幼,而垃圾回收 以下簡稱GC(Garbage Collection)機制作為內(nèi)存管理最重要的一個部分灌曙,是我們必須要掌握的耗跛。今天就分享下我對 垃圾回收機制分代回收策略 的理解.

目錄


  • 1. 背景

  • 2. 兩種回收機制

    • 2.1. 引用計數(shù)
    • 2.2. 可達性分析
  • 3. 回收算法

    • 3.1. 標記清除算法
    • 3.2. 復(fù)制算法
    • 3.3. 標記壓縮算法
  • 4. 分代回收策略

    • 4.1. 新生代
    • 4.2. 老生代
  • 5. 四大引用


1. 背景

一般來講裕照,在我們編程的過程中是會不斷的往內(nèi)存中寫入數(shù)據(jù)的,而這些數(shù)據(jù)用完了要及時從內(nèi)存中清理调塌,否則會引發(fā)OutOfMemory(內(nèi)存溢出) 晋南,所以每個編程者都必須遵從這一原則。聽說(我也不懂C語言~)在C語言階段羔砾,垃圾是需要編程者自己手動回收的负间,而我們Javaer相對來說就要幸福多了,因為JVM存在GC機制姜凄,也就是說JVM會幫我們自動清理垃圾政溃,但幸福也是有代價的,因為總是會有些垃圾對象陰差陽錯的避開GC算法态秧,這一現(xiàn)象也稱之為內(nèi)存泄漏董虱,所以只有掌握了GC機制才能避免寫出內(nèi)存泄漏的程序。

2. 兩種回收機制

2.1 引用計數(shù)

什么是引用計數(shù)呢申鱼?打個比方A a = new A()愤诱,代碼中 A 對象被引用 a 所持有,此時引用計數(shù)就會 +1 捐友,如果 a 將引用置為 nulla = null此時對象 A 的引用計數(shù)就會變?yōu)?0 淫半,GC算法檢測到 A 對象引用計數(shù)為 0 就會將其回收。很簡單匣砖,但引用計數(shù)存在一定弊端

場景如下:

A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;

執(zhí)行完上述代碼后 AB 對象會被回收嗎科吭?看似引用都已經(jīng)置為 null ,但實際上 abnext 分別持有對方引用脆粥,形成了一種相互持有引用的局面砌溺,導(dǎo)致AB 即使成了垃圾對象且不能被回收。有些同學(xué)可能會說变隔,內(nèi)存泄漏太容易看出來了, ab 置空前將各自的 next 置為空不就完了蟹倾。嗯匣缘,這樣說沒錯猖闪,但是在實際業(yè)務(wù)中面對龐大的業(yè)務(wù)邏輯內(nèi)存泄漏是很難一眼看出的。所以JVM在后來摒棄了引用計數(shù)肌厨,采用了可達性分析培慌。

2.2 可達性分析

可達性分析其實是數(shù)學(xué)中的一個概念,在JVM中柑爸,會將一些特殊的引用作為 GcRoot 吵护,如果通過 GcRoot 可以訪達的對象不會被當(dāng)作垃圾對象。換種方式說就是表鳍,一個對象被 GcRoot 直接 或 間接持有馅而,那么該對象就不會被當(dāng)作垃圾對象。用一張圖表示大概就是這個樣子:

image

圖中A譬圣、B、C、D可以被 GcRoot 訪達砖瞧,所以不會被回收嘀趟。E、F不能被 GcRoot 訪達绳姨,所以會被標記為垃圾對象登澜。最典型的是G、H飘庄,雖說相互引用脑蠕,但不能被 GcRoot 訪達,所以也會被標記為垃圾對象竭宰。綜上所述: 可達性分析 可以解決 引用計數(shù) 中 對象相互引用 不能被回收的問題空郊。

什么類型的引用可作為 GcRoot 呢。 大概有如下四種:

  • 棧中局部變量
  • 方法區(qū)中靜態(tài)變量
  • 方法區(qū)中常量
  • 本地方法棧JNI的引用對象

注意點

千萬不要把引用和對象兩個概念混淆切揭,對象是實實在在存在于內(nèi)存中的狞甚,而引用只是一個變量/常量并持有對象在內(nèi)存中的地址指。

下面我來通過一些代碼來驗證幾種 GcRoot

局部變量

筆者是用Android代碼進行調(diào)試廓旬,不懂Android的同學(xué)把onCreate視為main方法即可哼审。

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        method();
    }

    private void method(){
        Log.i("test","method start");
        A a = new A();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.i("test","method end");
    }

    class A{
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize A");
        }
    }
}

提示

  • 在java中一個對象被回收會調(diào)用其finalize方法
  • JVM中垃圾回收是在一個單獨線程進行。為了更好的驗證效果孕豹,在此加2000毫秒延時

打印結(jié)果如下:

17:58:57.526   method start
17:58:59.526   method end
17:58:59.591   finalize A

method 方法執(zhí)行時間是2000毫秒涩盾,對象Amethod 方法結(jié)束立即被回收。所以可以認定棧中局部變量可作為 GcRoot

本地方法區(qū)靜態(tài)變量

public class MyApp extends Application {

    private static A a;
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("test","onCreate");
        a = new A();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a = null;
        Log.i("test","a = null");
    }
}

打印結(jié)果如下:

18:12:35.988   a = new A()
18:12:38.028   a = null
18:12:38.096   finalize A

創(chuàng)建一個 A 對象賦值給靜態(tài)變量 a 励背, 2000毫秒后將靜態(tài)變量 a 置為空春霍。通過日志可以看出對象 A 在靜態(tài)變量 a 置空后被立即回收。所以可以認定靜態(tài)變量可作為 GcRoot

方法區(qū)常量與靜態(tài)變量驗證過程完全一致叶眉,關(guān)于native 驗證過程比較復(fù)雜址儒,感興趣的同學(xué)可自行驗證芹枷。

驗證成員變量是否可作為 GcRoot

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        A a = new A();
        B b = new B();
        a.b = b;
        a = null;
    }

    class A{
        B b;
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize A");
        }
    }

    class B{
        @Override
        protected void finalize() throws Throwable {
            Log.i("test","finalize B");
        }
    }
}

打印結(jié)果如下:

13:14:58.999   finalize A
13:14:58.999   finalize B

通過日志可以看出,A莲趣、B 兩個對象都被回收鸳慈。雖然 B 對象被 A 對象中的 b 引用所持有,但成員變量不能被作為 GcRoot喧伞, 所以B 對象不可達走芋,進而會被當(dāng)作垃圾。

3. 回收算法

上一小結(jié)描述了 GC 機制潘鲫,但具體實現(xiàn)還是要靠算法翁逞,下面我簡單描述一下幾種常見的 GC算法

3.1. 標記清除算法

獲取所有的 GcRoot 遍歷內(nèi)存中所有的對象次舌,如果可以被 GcRoot 就加個標記熄攘,剩下所有的對象都將視為垃圾被清除。

  • 優(yōu)點:實現(xiàn)簡單彼念,執(zhí)行效率高
  • 缺點:容易產(chǎn)生 內(nèi)存碎片(可用內(nèi)存分布比較分散)挪圾,如果需要申請大塊連續(xù)內(nèi)存可能會頻繁觸發(fā) GC

3.2. 復(fù)制算法

將內(nèi)存分為兩塊,每次只是用其中一塊逐沙。首先遍歷所有對象哲思,將可用對象復(fù)制到另一塊內(nèi)存中,此時上一塊內(nèi)存可視為全是垃圾吩案,清理后將新內(nèi)存塊置為當(dāng)前可用棚赔。如此反復(fù)進行

  • 優(yōu)點:解決了內(nèi)存碎片的問題
  • 缺點:需要按順序分配內(nèi)存,可用內(nèi)存變?yōu)樵瓉淼囊话搿?/li>

3.3. 標記壓縮算法

獲取所有的 GcRoot 徘郭, GcRoot 開始從遍歷內(nèi)存中所有的對象靠益,將可用對象壓縮到另一端,再將垃圾對象清除残揉。實則是犧牲時間復(fù)雜度來降低空間復(fù)雜度

  • 優(yōu)點:解決了標記清除的 內(nèi)存碎片 胧后,也不需要復(fù)制算法中的 內(nèi)存分塊
  • 缺點:仍需要將對象進行移動,執(zhí)行效率略低抱环。

4. 分代回收策略

在JVM中 垃圾回收器 是很繁忙的壳快,如果一個對象存活時間較長,避免重復(fù) 創(chuàng)建/回收垃圾回收器 進一步造成負擔(dān)镇草,能不能犧牲點內(nèi)存把它緩存起來? 答案是肯定的眶痰。JVM制定了 分代回收策略 為每個對象設(shè)置生命周期
,堆內(nèi)存會劃分不同的區(qū)域竖伯,來存儲各生命周期的對象。一般情況下對象的生命周期有 新生代黔夭、老年代羽嫡、永久代(java 8已廢棄)本姥。

4.1. 新生代

首先來看新生代內(nèi)存結(jié)構(gòu)示意圖:

image

按照8:1:1將新生代內(nèi)存分為 Eden、SurvivorA杭棵、SurvivorB

新生代內(nèi)存工作流程:

  • 當(dāng)一個對象剛被創(chuàng)建時會放到 Eden 區(qū)域,當(dāng) Eden 區(qū)域即將存滿時做一次垃圾回收先舷,將當(dāng)前存活的對象復(fù)制到 SurvivorA ,隨后將 Eden 清空
  • 當(dāng)Eden 下一次存滿時滓侍,再做一次垃圾回收蒋川,先將存活對象復(fù)制到 SurvivorB ,再把 EdenSurvivorA 所有對象進行回收撩笆,
  • 當(dāng)Eden 再一次存滿時,再做一次垃圾回收夕冲,將存活對象復(fù)制到 SurvivorA,再把 EdenSurvivorB對象進行回收泣栈。如此反復(fù)進行大概 15 次弥姻,將最終依舊存活的對象放入到老年代區(qū)域。

新生代工作流程與 復(fù)制算法 應(yīng)用場景較為吻合庭敦,都是以復(fù)制為核心,所以會采用復(fù)制算法颠悬。

4.2. 老年代

根據(jù)對 上一小節(jié) 我們可以得知 當(dāng)一個對象存活時間較久會被存入到 老年代 區(qū)域定血。 老年代 區(qū)即將被存滿時會做一次垃圾回收,



所以 老年代 區(qū)域特點是存活對象多灾票、垃圾對象少茫虽,采用標記壓縮 算法時移動少既们、也不會產(chǎn)生內(nèi)存碎片正什。所以老年代 區(qū)域可以選用 標記壓縮 算法進一步提升效率。

5. 四大引用

在我們開發(fā)程序的過程中婴氮,避免不了會創(chuàng)建一些比較大的對象,比如Android中用于承載像素信息的Bitmap荣暮,使用稍有不當(dāng)就會造成內(nèi)存泄漏罩驻,如果存在大量類似對象對內(nèi)存影響還是蠻大的。



為了盡可能避免上述情況的出現(xiàn)砾跃,JVM為我們提供了四種對象引用方式:強引用爽哎、軟引用、弱引用课锌、虛引用 供我們選擇,下面我用一張表格來做一下類比

  • 假設(shè)以下所述對象可被 GcRoot 訪達
引用類型 回收時機
強引用 絕不會被回收(默認)
軟引用 內(nèi)存不足時回收
弱引用 第一次觸發(fā)GC時就會被回收
虛引用 隨時都會被回收雏胃,不存在實際意義






參考文獻:《Android 工程師進階 34 講》 第二講

結(jié)語

文章從五個方面描述了 GC 機制志鞍。

  • GC 機制的誕生是為了提升開發(fā)者的效率
  • 可達性分析 解決的 引用計數(shù) 相互引用的問題
  • 不同場景 運用不同 GC 算法可以提升效率
  • 分代回收策略 進一步提升 GC 效率
  • 巧妙運用 四大引用 可以一定程度解決 內(nèi)存泄漏
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末固棚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子厂汗,更是在濱河造成了極大的恐慌呜师,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衷畦,死亡現(xiàn)場離奇詭異,居然都是意外死亡祈争,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門暖释,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墨吓,“玉大人纹磺,你說我怎么就攤上這事∶刂ⅲ” “怎么了式矫?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長聪廉。 經(jīng)常有香客問我故慈,道長,這世上最難降的妖魔是什么察绷? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任拆撼,我火速辦了婚禮,結(jié)果婚禮上闸度,老公的妹妹穿的比我還像新娘筋岛。我一直安慰自己娶视,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布寝凌。 她就那樣靜靜地躺著孝赫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伐债。 梳的紋絲不亂的頭發(fā)上致开,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音虹蒋,去河邊找鬼飒货。 笑死,一個胖子當(dāng)著我的面吹牛塘辅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哲银,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沮榜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了草巡?” 一聲冷哼從身側(cè)響起型酥,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郁竟,沒想到半個月后由境,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蓖议,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡讥蟆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年瘸彤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片质况。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡结榄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出潭陪,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布黎炉,位于F島的核電站醋拧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏丹壕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一缭乘、第九天 我趴在偏房一處隱蔽的房頂上張望琉用。 院中可真熱鬧,春花似錦邑时、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捷枯。三九已至件缸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間他炊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工蚕苇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凿叠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓蹬碧,卻偏偏與公主長得像炒刁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子翔始,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355