我對JVM中GC機制的理解

前言

做爲一名Java編程者,想要往高級進階,內存管理每每是避不開的環節,而垃圾回收 如下簡稱GC(Garbage Collection)機制做爲內存管理最重要的一個部分,是咱們必需要掌握的。今天就分享下我對 垃圾回收機制分代回收策略 的理解.java

目錄


  • 1. 背景算法

  • 2. 兩種回收機制編程

    • 2.1. 引用計數
    • 2.2. 可達性分析
  • 3. 回收算法緩存

    • 3.1. 標記清除算法
    • 3.2. 複製算法
    • 3.3. 標記壓縮算法
  • 4. 分代回收策略bash

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


1. 背景

通常來說,在咱們編程的過程當中是會不斷的往內存中寫入數據的,而這些數據用完了要及時從內存中清理,不然會引起OutOfMemory(內存溢出) ,因此每一個編程者都必須聽從這一原則。據說(我也不懂C語言~)在C語言階段,垃圾是須要編程者本身手動回收的,而咱們Javaer相對來講就要幸福多了,由於JVM存在GC機制,也就是說JVM會幫咱們自動清理垃圾,但幸福也是有代價的,由於老是會有些垃圾對象陰差陽錯的避開GC算法,這一現象也稱之爲內存泄漏,因此只有掌握了GC機制才能避免寫出內存泄漏的程序。spa

2. 兩種回收機制

2.1 引用計數

什麼是引用計數呢?打個比方A a = new A(),代碼中 A 對象被引用 a 所持有,此時引用計數就會 +1 ,若是 a 將引用置爲 nulla = null此時對象 A 的引用計數就會變爲 0 ,GC算法檢測到 A 對象引用計數爲 0 就會將其回收。很簡單,但引用計數存在必定弊端線程

場景以下:3d

A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
複製代碼

執行完上述代碼後 AB 對象會被回收嗎?看似引用都已經置爲 null ,但實際上 abnext 分別持有對方引用,造成了一種相互持有引用的局面,致使AB 即便成了垃圾對象且不能被回收。有些同窗可能會說,內存泄漏太容易看出來了, ab 置空前將各自的 next 置爲空不就完了。嗯,這樣說沒錯,可是在實際業務中面對龐大的業務邏輯內存泄漏是很難一眼看出的。因此JVM在後來摒棄了引用計數,採用了可達性分析。調試

2.2 可達性分析

可達性分析實際上是數學中的一個概念,在JVM中,會將一些特殊的引用做爲 GcRoot ,若是經過 GcRoot 能夠訪達的對象不會被看成垃圾對象。換種方式說就是,一個對象被 GcRoot 直接 或 間接持有,那麼該對象就不會被看成垃圾對象。用一張圖表示大概就是這個樣子:

圖中A、B、C、D可以被 GcRoot 訪達,因此不會被回收。E、F不能被 GcRoot 訪達,因此會被標記爲垃圾對象。最典型的是G、H,雖然說相互引用,但不能被 GcRoot 訪達,因此也會被標記爲垃圾對象。綜上所述: 可達性分析 能夠解決 引用計數 中 對象相互引用 不能被回收的問題。

什麼類型的引用可做爲 GcRoot 呢。 大概有以下四種:

  • 棧中局部變量
  • 方法區中靜態變量
  • 方法區中常量
  • 本地方法棧JNI的引用對象

注意點

千萬不要把引用和對象兩個概念混淆,對象是實實在在存在於內存中的,而引用只是一個變量/常量並持有對象在內存中的地址指。

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

局部變量

筆者是用Android代碼進行調試,不懂Android的同窗把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中一個對象被回收會調用其finalize方法
  • JVM中垃圾回收是在一個單獨線程進行。爲了更好的驗證效果,在此加2000毫秒延時

打印結果以下:

17:58:57.526   method start
17:58:59.526   method end
17:58:59.591   finalize A
複製代碼

method 方法執行時間是2000毫秒,對象Amethod 方法結束當即被回收。因此能夠認定棧中局部變量可做爲 GcRoot

本地方法區靜態變量

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");
    }
}
複製代碼

打印結果以下:

18:12:35.988   a = new A()
18:12:38.028   a = null
18:12:38.096   finalize A
複製代碼

建立一個 A 對象賦值給靜態變量 a , 2000毫秒後將靜態變量 a 置爲空。經過日誌能夠看出對象 A 在靜態變量 a 置空後被當即回收。因此能夠認定靜態變量可做爲 GcRoot

方法區常量與靜態變量驗證過程徹底一致,關於native 驗證過程比較複雜,感興趣的同窗可自行驗證。

驗證成員變量是否可做爲 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");
        }
    }
}
複製代碼

打印結果以下:

13:14:58.999   finalize A
13:14:58.999   finalize B
複製代碼

經過日誌能夠看出,A、B 兩個對象都被回收。雖然 B 對象被 A 對象中的 b 引用所持有,但成員變量不能被做爲 GcRoot, 因此B 對象不可達,進而會被看成垃圾。

3. 回收算法

上一小結描述了 GC 機制,但具體實現仍是要靠算法,下面我簡單描述一下幾種常見的 GC算法

3.1. 標記清除算法

獲取全部的 GcRoot 遍歷內存中全部的對象,若是能夠被 GcRoot 就加個標記,剩下全部的對象都將視爲垃圾被清除。

  • 優勢:實現簡單,執行效率高
  • 缺點:容易產生 內存碎片(可用內存分佈比較分散),若是須要申請大塊連續內存可能會頻繁觸發 GC

3.2. 複製算法

將內存分爲兩塊,每次只是用其中一塊。首先遍歷全部對象,將可用對象複製到另外一塊內存中,此時上一塊內存可視爲全是垃圾,清理後將新內存塊置爲當前可用。如此反覆進行

  • 優勢:解決了內存碎片的問題
  • 缺點:須要按順序分配內存,可用內存變爲原來的一半。

3.3. 標記壓縮算法

獲取全部的 GcRootGcRoot 開始從遍歷內存中全部的對象,將可用對象壓縮到另外一端,再將垃圾對象清除。實則是犧牲時間複雜度來下降空間複雜度

  • 優勢:解決了標記清除的 內存碎片 ,也不須要複製算法中的 內存分塊
  • 缺點:仍須要將對象進行移動,執行效率略低。

4. 分代回收策略

在JVM中 垃圾回收器 是很繁忙的,若是一個對象存活時間較長,避免重複 建立/回收垃圾回收器 進一步形成負擔,能不能犧牲點內存把它緩存起來? 答案是確定的。JVM制定了 分代回收策略 爲每一個對象設置生命週期 ,堆內存會劃分不一樣的區域,來存儲各生命週期的對象。通常狀況下對象的生命週期有 新生代、老年代、永久代(java 8已廢棄)

4.1. 新生代

首先來看新生代內存結構示意圖:

按照8:1:1將新生代內存分爲 Eden、SurvivorA、SurvivorB

新生代內存工做流程:

  • 當一個對象剛被建立時會放到 Eden 區域,當 Eden 區域即將存滿時作一次垃圾回收,將當前存活的對象複製到 SurvivorA ,隨後將 Eden 清空
  • Eden 下一次存滿時,再作一次垃圾回收,先將存活對象複製到 SurvivorB ,再把 EdenSurvivorA 全部對象進行回收,
  • Eden 再一次存滿時,再作一次垃圾回收,將存活對象複製到 SurvivorA,再把 EdenSurvivorB對象進行回收。如此反覆進行大概 15 次,將最終依舊存活的對象放入到老年代區域。

新生代工做流程與 複製算法 應用場景較爲吻合,都是以複製爲核心,因此會採用複製算法

4.2. 老年代

根據對 上一小節 咱們能夠得知 當一個對象存活時間較久會被存入到 老年代 區域。 老年代 區即將被存滿時會作一次垃圾回收,

因此 老年代 區域特色是存活對象多、垃圾對象少,採用標記壓縮 算法時移動少、也不會產生內存碎片。因此老年代 區域能夠選用 標記壓縮 算法進一步提高效率。

5. 四大引用

在咱們開發程序的過程當中,避免不了會建立一些比較大的對象,好比Android中用於承載像素信息的Bitmap,使用稍有不當就會形成內存泄漏,若是存在大量相似對象對內存影響仍是蠻大的。

爲了儘量避免上述狀況的出現,JVM爲咱們提供了四種對象引用方式:強引用、軟引用、弱引用、虛引用 供咱們選擇,下面我用一張表格來作一下類比

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





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

結語

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

  • GC 機制的誕生是爲了提高開發者的效率
  • 可達性分析 解決的 引用計數 相互引用的問題
  • 不一樣場景 運用不一樣 GC 算法能夠提高效率
  • 分代回收策略 進一步提高 GC 效率
  • 巧妙運用 四大引用 能夠必定程度解決 內存泄漏
相關文章
相關標籤/搜索